tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades.

If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples

Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html

About Tinkoff Invest API: https://tinkoff.github.io/investAPI/

Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is a python API to work with some methods of Tinkoff Open API using REST protocol.
   6It can view history, orders and market information. Also, you can open orders and trades.
   7
   8If you run this module as CLI program then it realizes simple logic: receiving a lot of options and execute one command.
   9**See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  10
  11**Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  12
  13About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
  14
  15Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
  16"""
  17
  18# Copyright (c) 2022 Gilmillin Timur Mansurovich
  19#
  20# Licensed under the Apache License, Version 2.0 (the "License");
  21# you may not use this file except in compliance with the License.
  22# You may obtain a copy of the License at
  23#
  24#     http://www.apache.org/licenses/LICENSE-2.0
  25#
  26# Unless required by applicable law or agreed to in writing, software
  27# distributed under the License is distributed on an "AS IS" BASIS,
  28# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  29# See the License for the specific language governing permissions and
  30# limitations under the License.
  31
  32
  33import sys
  34import os
  35from argparse import ArgumentParser
  36from importlib.metadata import version
  37
  38from datetime import datetime, timedelta
  39from dateutil.tz import tzlocal, tzutc
  40from time import sleep
  41
  42import re
  43import json
  44import requests
  45import traceback as tb
  46from typing import Union
  47
  48from multiprocessing import cpu_count
  49from multiprocessing.pool import ThreadPool
  50import pandas as pd
  51
  52from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  53
  54from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  55from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  56
  57import UniLogger as uLog  # Logger for TKSBrokerAPI
  58
  59
  60# --- Common technical parameters:
  61
  62PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  63uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  64uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  65uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  66
  67__version__ = "1.3"  # The "major.minor" version setup here, but build number define at the build-server only
  68
  69CPU_COUNT = cpu_count()  # host's real CPU count
  70CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  71
  72# --- Main constants:
  73
  74NANO = 0.000000001  # SI-constant nano = 10^-9
  75
  76
  77def NanoToFloat(units: str, nano: int) -> float:
  78    """
  79    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
  80
  81    `NanoToFloat(units="2", nano=500000000) -> 2.5`
  82
  83    `NanoToFloat(units="0", nano=50000000) -> 0.05`
  84
  85    :param units: integer string or integer parameter that represents the integer part of number
  86    :param nano: integer string or integer parameter that represents the fractional part of number
  87    :return: float view of number
  88    """
  89    return int(units) + int(nano) * NANO
  90
  91
  92def FloatToNano(number: float) -> dict:
  93    """
  94    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
  95
  96    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
  97
  98    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
  99
 100    :param number: float number
 101    :return: nano-type view of number: `{"units": "string", "nano": integer}`
 102    """
 103    splitByPoint = str(number).split(".")
 104    frac = 0
 105
 106    if len(splitByPoint) > 1:
 107        if len(splitByPoint[1]) <= 9:
 108            frac = int("{}{}".format(
 109                int(splitByPoint[1]),
 110                "0" * (9 - len(splitByPoint[1])),
 111            ))
 112
 113    if (number < 0) and (frac > 0):
 114        frac = -frac
 115
 116    return {"units": str(int(number)), "nano": frac}
 117
 118
 119def GetDatesAsString(start: str = None, end: str = None) -> tuple:
 120    """
 121    Create tuple of date and time strings with timezone parsed from user-friendly date.
 122
 123    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
 124
 125    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
 126    An error exception will occur if input date has incorrect format.
 127
 128    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
 129    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
 130    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
 131    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
 132
 133    Also, you can use keywords for start if `end=None`:
 134    `today` (from 00:00:00 to the end of current day),
 135    `yesterday` (-1 day from 00:00:00 to 23:59:59),
 136    `week` (-7 day from 00:00:00 to the end of current day),
 137    `month` (-30 day from 00:00:00 to the end of current day),
 138    `year` (-365 day from 00:00:00 to the end of current day),
 139
 140    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
 141             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
 142             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
 143    """
 144    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
 145    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
 146    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
 147
 148    # time between start and the end of the current day:
 149    if start is None or start.lower() == "today":
 150        pass
 151
 152    # from start of the last day to the end of the last day:
 153    elif start.lower() == "yesterday":
 154        s -= timedelta(days=1)
 155        e -= timedelta(days=1)
 156
 157    # week (-7 day from 00:00:00 to the end of the current day):
 158    elif start.lower() == "week":
 159        s -= timedelta(days=6)  # +1 current day already taken into account
 160
 161    # month (-30 day from 00:00:00 to the end of current day):
 162    elif start.lower() == "month":
 163        s -= timedelta(days=29)  # +1 current day already taken into account
 164
 165    # year (-365 day from 00:00:00 to the end of current day):
 166    elif start.lower() == "year":
 167        s -= timedelta(days=364)  # +1 current day already taken into account
 168
 169    # -N days ago to the end of current day:
 170    elif start.startswith('-') and start[1:].isdigit():
 171        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
 172
 173    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
 174    else:
 175        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
 176        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
 177
 178    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
 179    s = s.strftime(TKS_DATE_TIME_FORMAT)
 180    e = e.strftime(TKS_DATE_TIME_FORMAT)
 181
 182    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
 183
 184    return s, e
 185
 186
 187class TinkoffBrokerServer:
 188    """
 189    This class implements methods to work with Tinkoff broker server.
 190
 191    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 192
 193    About `token`: https://tinkoff.github.io/investAPI/token/
 194    """
 195    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 196        """
 197        Main class init.
 198
 199        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 200        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 201                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 202        :param useCache: use default cache file with raw data to use instead of `iList`.
 203                         True by default. Cache is auto-update if new day has come.
 204                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 205        :param defaultCache: path to default cache file. `dump.json` by default.
 206        """
 207        if token is None or not token:
 208            try:
 209                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 210                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 211
 212            except KeyError:
 213                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 214                raise Exception("Token required")
 215
 216        else:
 217            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 218            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 219
 220        if accountId is None or not accountId:
 221            try:
 222                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 223                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 224
 225            except KeyError:
 226                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 227
 228        else:
 229            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 230            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 231
 232        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 233        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 234
 235        Latest version: https://pypi.org/project/tksbrokerapi/
 236        """
 237
 238        self.aliases = TKS_TICKER_ALIASES
 239        """Some aliases instead official tickers.
 240
 241        See also: `TKSEnums.TKS_TICKER_ALIASES`
 242        """
 243
 244        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 245
 246        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 247
 248        self.ticker = ""
 249        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 250
 251        See also: `SearchByTicker()`, `SearchInstruments()`.
 252        """
 253
 254        self.figi = ""
 255        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 256
 257        See also: `SearchByFIGI()`, `SearchInstruments()`.
 258        """
 259
 260        self.depth = 1
 261        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 262
 263        See also: `GetCurrentPrices()`.
 264        """
 265
 266        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 267        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 268
 269        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 270        """
 271
 272        uLogger.debug("Broker API server: {}".format(self.server))
 273
 274        self.timeout = 15
 275        """Server operations timeout in seconds. Default: `15`.
 276
 277        See also: `SendAPIRequest()`.
 278        """
 279
 280        self.headers = {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {}".format(self.token)}
 281        """Headers which send in every request to broker server. Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 282
 283        See also: `SendAPIRequest()`.
 284        """
 285
 286        self.body = None
 287        """Request body which send to broker server. Default: `None`.
 288
 289        See also: `SendAPIRequest()`.
 290        """
 291
 292        self.historyFile = None
 293        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe.
 294
 295        See also: `History()`.
 296        """
 297
 298        self.htmlHistoryFile = "index.html"
 299        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 300
 301        See also: `ShowHistoryChart()`.
 302        """
 303
 304        self.instrumentsFile = "instruments.md"
 305        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 306
 307        See also: `ShowInstrumentsInfo()`.
 308        """
 309
 310        self.searchResultsFile = "search-results.md"
 311        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 312
 313        See also: `SearchInstruments()`.
 314        """
 315
 316        self.pricesFile = "prices.md"
 317        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 318
 319        See also: `GetListOfPrices()`.
 320        """
 321
 322        self.infoFile = "info.md"
 323        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 324
 325        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 326        """
 327
 328        self.bondsXLSXFile = "ext-bonds.xlsx"
 329        """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 
 330        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 331
 332        See also: `ExtendBondsData()`.
 333        """
 334
 335        self.calendarFile = "calendar.md"
 336        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 337        
 338        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 339
 340        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 341        """
 342
 343        self.overviewFile = "overview.md"
 344        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 345
 346        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 347        """
 348
 349        self.overviewDigestFile = "overview-digest.md"
 350        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 351
 352        See also: `Overview()` with parameter `details="digest"`.
 353        """
 354
 355        self.overviewPositionsFile = "overview-positions.md"
 356        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 357
 358        See also: `Overview()` with parameter `details="positions"`.
 359        """
 360
 361        self.overviewOrdersFile = "overview-orders.md"
 362        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 363
 364        See also: `Overview()` with parameter `details="orders"`.
 365        """
 366
 367        self.overviewAnalyticsFile = "overview-analytics.md"
 368        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 369
 370        See also: `Overview()` with parameter `details="analytics"`.
 371        """
 372
 373        self.reportFile = "deals.md"
 374        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 375
 376        See also: `Deals()`.
 377        """
 378
 379        self.withdrawalLimitsFile = "limits.md"
 380        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 381
 382        See also: `OverviewLimits()` and `RequestLimits()`.
 383        """
 384
 385        self.userInfoFile = "user-info.md"
 386        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 387
 388        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 389        """
 390
 391        self.userAccountsFile = "accounts.md"
 392        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 393
 394        See also: `OverviewAccounts()`, `RequestAccounts()`.
 395        """
 396
 397        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 398        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 399
 400        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 401
 402        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 403        """
 404
 405        self.iList = None  # init iList for raw instruments data
 406        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 407        
 408        See also: `Listing()`, `DumpInstruments()`.
 409        """
 410
 411        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 412        if useCache:
 413            if os.path.exists(self.iListDumpFile):
 414                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 415                curTime = datetime.now(tzutc())
 416
 417                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 418                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 419
 420                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 421
 422                else:
 423                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 424
 425                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
 426                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 427
 428            else:
 429                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 430                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 431
 432        else:
 433            self.iList = self.Listing()  # request new raw instruments data from broker server
 434            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 435
 436        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 437        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 438
 439        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 440        """
 441
 442    @staticmethod
 443    def _ParseJSON(rawData="{}", debug: bool = False) -> dict:
 444        """
 445        Parse JSON from response string.
 446
 447        :param rawData: this is a string with JSON-formatted text.
 448        :param debug: if `True` then print more debug information.
 449        :return: JSON (dictionary), parsed from server response string.
 450        """
 451        if debug:
 452            uLogger.debug("Raw text body:")
 453            uLogger.debug(rawData)
 454
 455        responseJSON = json.loads(rawData) if rawData else {}
 456
 457        if debug:
 458            uLogger.debug("JSON formatted:")
 459            for jsonLine in json.dumps(responseJSON, indent=4).split('\n'):
 460                uLogger.debug(jsonLine)
 461
 462        return responseJSON
 463
 464    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
 465        """
 466        Send GET or POST request to broker server and receive JSON object.
 467
 468        self.header: must be defining with dictionary of headers.
 469        self.body: if define then used as request body. None by default.
 470        self.timeout: global request timeout, 15 seconds by default.
 471        :param url: url with REST request.
 472        :param reqType: send "GET" or "POST" request. "GET" by default.
 473        :param retry: how many times retry after first request if an 5xx server errors occurred.
 474        :param pause: sleep time in seconds between retries.
 475        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
 476        :return: response JSON (dictionary) from broker.
 477        """
 478        if reqType not in ("GET", "POST"):
 479            uLogger.error("You can define request type: 'GET' or 'POST'!")
 480            raise Exception("Incorrect value")
 481
 482        if debug:
 483            uLogger.debug("Request parameters:")
 484            uLogger.debug("    - REST API URL: {}".format(url))
 485            uLogger.debug("    - request type: {}".format(reqType))
 486            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
 487            uLogger.debug("    - body: {}".format(self.body))
 488
 489        # fast hack to avoid all operations with some tickers/FIGI
 490        responseJSON = {}
 491        oK = True
 492        for item in self.exclude:
 493            if item in url:
 494                if debug:
 495                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 496
 497                oK = False
 498                break
 499
 500        if oK:
 501            counter = 0
 502            response = None
 503            errMsg = ""
 504
 505            while not response and counter <= retry:
 506                if reqType == "GET":
 507                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 508
 509                if reqType == "POST":
 510                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 511
 512                if debug:
 513                    uLogger.debug("Response:")
 514                    uLogger.debug("    - status code: {}".format(response.status_code))
 515                    uLogger.debug("    - reason: {}".format(response.reason))
 516                    uLogger.debug("    - body length: {}".format(len(response.text)))
 517                    uLogger.debug("    - headers: {}".format(response.headers))
 518
 519                # Server returns some headers:
 520                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 521                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 522                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 523                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 524                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 525                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 526                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 527                    sleep(rateLimitWait)
 528
 529                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 530                if 400 <= response.status_code < 500:
 531                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 532                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 533                    counter = retry + 1
 534
 535                if 500 <= response.status_code < 600:
 536                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 537                    uLogger.debug("    - not oK, {}".format(errMsg))
 538                    counter += 1
 539
 540                    if counter <= retry:
 541                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 542                        sleep(pause)
 543
 544            responseJSON = self._ParseJSON(response.text)
 545
 546            if errMsg:
 547                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 548                uLogger.error("    - not oK, {}".format(errMsg))
 549
 550        return responseJSON
 551
 552    def _IUpdater(self, iType: str) -> tuple:
 553        """
 554        Request instrument by type from server. See available API methods for instruments:
 555        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 556        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 557        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 558        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 559        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 560
 561        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 562        :return: tuple with iType name and list of available instruments of current type for defined user token.
 563        """
 564        result = []
 565
 566        if iType in TKS_INSTRUMENTS:
 567            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 568
 569            # all instruments have the same body in API v2 requests:
 570            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 571            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 572            result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"]
 573
 574        return iType, result
 575
 576    def _IWrapper(self, kwargs):
 577        """
 578        Wrapper runs instrument's update method `_IUpdater()`.
 579        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 580        """
 581        return self._IUpdater(**kwargs)
 582
 583    def Listing(self) -> dict:
 584        """
 585        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 586
 587        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 588        """
 589        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 590        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 591
 592        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 593        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 594        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 595
 596        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 597        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 598        poolUpdater.close()
 599
 600        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 601        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 602        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 603
 604        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 605        for iType in iList.keys():
 606            for ticker in iList[iType]:
 607                iList[iType][ticker]["type"] = iType
 608
 609                if "minPriceIncrement" in iList[iType][ticker].keys():
 610                    iList[iType][ticker]["step"] = NanoToFloat(
 611                        iList[iType][ticker]["minPriceIncrement"]["units"],
 612                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 613                    )
 614
 615                else:
 616                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 617
 618        return iList
 619
 620    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 621        """
 622        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 623
 624        See also: `DumpInstruments()`, `Listing()`.
 625
 626        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 627                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 628        """
 629        if self.iListDumpFile is None or not self.iListDumpFile:
 630            uLogger.error("Output name of dump file must be defined!")
 631            raise Exception("Filename required")
 632
 633        if not self.iList or forceUpdate:
 634            self.iList = self.Listing()
 635
 636        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 637
 638        # Save as XLSX with separated sheets for every type of instruments:
 639        with pd.ExcelWriter(
 640                path=xlsxDumpFile,
 641                date_format=TKS_DATE_FORMAT,
 642                datetime_format=TKS_DATE_TIME_FORMAT,
 643                mode="w",
 644        ) as writer:
 645            for iType in TKS_INSTRUMENTS:
 646                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 647                df = df[sorted(df)]  # sorted by column names
 648                df = df.applymap(
 649                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 650                    na_action="ignore",
 651                )  # converting numbers from nano-type to float in every cell
 652                df.to_excel(
 653                    writer,
 654                    sheet_name=iType,
 655                    encoding="UTF-8",
 656                    freeze_panes=(1, 1),
 657                )  # saving as XLSX-file with freeze first row and column as headers
 658
 659        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 660
 661    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 662        """
 663        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 664        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 665
 666        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 667
 668        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 669                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 670        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 671        """
 672        if self.iListDumpFile is None or not self.iListDumpFile:
 673            uLogger.error("Output name of dump file must be defined!")
 674            raise Exception("Filename required")
 675
 676        if not self.iList or forceUpdate:
 677            self.iList = self.Listing()
 678
 679        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 680        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 681            fH.write(jsonDump)
 682
 683        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 684
 685        return jsonDump
 686
 687    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 688        """
 689        Show information about one instrument defined by json data and prints it in Markdown format.
 690
 691        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 692
 693        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 694        :param show: if `True` then also printing information about instrument and its current price.
 695        :return: multilines text in Markdown format with information about one instrument.
 696        """
 697        splitLine = "|                                                             |                                                        |\n"
 698        infoText = ""
 699
 700        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 701            info = [
 702                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 703                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 704                "| Parameters                                                  | Values                                                 |\n",
 705                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 706                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 707                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 708            ]
 709
 710            if "sector" in iJSON.keys() and iJSON["sector"]:
 711                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 712
 713            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 714                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 715                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 716            )))
 717
 718            info.extend([
 719                splitLine,
 720                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 721                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 722            ])
 723
 724            if "isin" in iJSON.keys() and iJSON["isin"]:
 725                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 726
 727            if "classCode" in iJSON.keys():
 728                info.append("| Class Code:                                                 | {:<54} |\n".format(iJSON["classCode"]))
 729
 730            info.extend([
 731                splitLine,
 732                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 733                splitLine,
 734                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 735                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 736                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 737            ])
 738
 739            if iJSON["figi"]:
 740                self.figi = iJSON["figi"]
 741                iJSON = iJSON | self.RequestTradingStatus()
 742
 743                info.extend([
 744                    splitLine,
 745                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 746                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 747                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 748                ])
 749
 750            info.append(splitLine)
 751
 752            if "type" in iJSON.keys() and iJSON["type"]:
 753                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 754
 755            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 756                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 757
 758            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 759                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 760
 761            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 762                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 763
 764            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 765                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 766
 767            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 768                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 769
 770            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 771                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 772
 773            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 774                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 775
 776            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 777                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 778
 779            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 780                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 781
 782            if "currency" in iJSON.keys():
 783                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 784
 785            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 786                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 787
 788            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 789                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 790
 791            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 792                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 793
 794            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 795                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 796
 797            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 798                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 799
 800            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 801                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 802
 803            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 804                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 805
 806            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 807                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 808
 809            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 810                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 811
 812            iExt = None
 813            if iJSON["type"] == "Bonds":
 814                info.extend([
 815                    splitLine,
 816                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 817                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 818                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 819                        iJSON["nominal"]["currency"],
 820                    )),
 821                ])
 822
 823                if "floatingCouponFlag" in iJSON.keys():
 824                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 825
 826                if "amortizationFlag" in iJSON.keys():
 827                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 828
 829                info.append(splitLine)
 830
 831                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 832                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 833
 834                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 835
 836                info.extend([
 837                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 838                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 839                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 840                ])
 841
 842                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 843                    info.append("| Current Accrued Interest (ACI):                             | {:<54} |\n".format("{:.2f} {}".format(
 844                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 845                        iJSON["aciValue"]["currency"]
 846                    )))
 847
 848            if "currentPrice" in iJSON.keys():
 849                info.append(splitLine)
 850
 851                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 852                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 853
 854                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 855                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 856                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 857                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 858                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 859
 860                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 861                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 862
 863                info.extend([
 864                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 865                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 866                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 867                    )),
 868                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 869                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 870                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 871                    )),
 872                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 873                        "{:.2f}%{}".format(
 874                            iJSON["currentPrice"]["changes"],
 875                            " ({}{:.2f} {})".format(
 876                                "+" if bondChangesDelta > 0 else "",
 877                                bondChangesDelta,
 878                                aciCurrency
 879                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 880                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 881                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 882                                currency
 883                            ),
 884                        )
 885                    ),
 886                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 887                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 888                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 889                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 890                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 891                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 892                    )),
 893                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 894                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 895                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 896                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 897                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 898                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 899                    )),
 900                ])
 901
 902            if "lot" in iJSON.keys():
 903                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 904
 905            if "step" in iJSON.keys() and iJSON["step"] != 0:
 906                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 907
 908            # Add bond payment calendar:
 909            if iJSON["type"] == "Bonds":
 910                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 911                info.extend(["\n", strCalendar])
 912
 913            infoText += "".join(info)
 914
 915            if show:
 916                uLogger.info("{}".format(infoText))
 917
 918            else:
 919                uLogger.debug("{}".format(infoText))
 920
 921            if self.infoFile is not None:
 922                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 923                    fH.write(infoText)
 924
 925                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 926
 927        return infoText
 928
 929    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 930        """
 931        Search and return raw broker's information about instrument by its ticker.
 932        `ticker` must be defined! If debug=True then print all debug messages.
 933
 934        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 935        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 936        :param debug: if `True` then print all debug console messages.
 937        :return: JSON formatted data with information about instrument.
 938        """
 939        tickerJSON = {}
 940        if debug:
 941            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 942
 943        if not self.ticker:
 944            uLogger.warning("self.ticker variable is not be empty!")
 945
 946        else:
 947            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 948                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 949                raise Exception("Instrument not allowed")
 950
 951            if not self.iList:
 952                self.iList = self.Listing()
 953
 954            if self.ticker in self.iList["Shares"].keys():
 955                tickerJSON = self.iList["Shares"][self.ticker]
 956                if debug:
 957                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 958
 959            elif self.ticker in self.iList["Currencies"].keys():
 960                tickerJSON = self.iList["Currencies"][self.ticker]
 961                if debug:
 962                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 963
 964            elif self.ticker in self.iList["Bonds"].keys():
 965                tickerJSON = self.iList["Bonds"][self.ticker]
 966                if debug:
 967                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 968
 969            elif self.ticker in self.iList["Etfs"].keys():
 970                tickerJSON = self.iList["Etfs"][self.ticker]
 971                if debug:
 972                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 973
 974            elif self.ticker in self.iList["Futures"].keys():
 975                tickerJSON = self.iList["Futures"][self.ticker]
 976                if debug:
 977                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 978
 979        if tickerJSON:
 980            self.figi = tickerJSON["figi"]
 981
 982            if requestPrice:
 983                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 984
 985                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 986                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 987
 988                else:
 989                    tickerJSON["currentPrice"]["changes"] = 0
 990
 991            if show:
 992                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 993
 994        else:
 995            if show:
 996                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 997
 998        return tickerJSON
 999
1000    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1001        """
1002        Search and return raw broker's information about instrument by its FIGI.
1003        `figi` must be defined! If debug=True then print all debug messages.
1004
1005        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1006        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1007        :param debug: if `True` then print all debug console messages.
1008        :return: JSON formatted data with information about instrument.
1009        """
1010        figiJSON = {}
1011        if debug:
1012            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1013
1014        if not self.figi:
1015            uLogger.warning("self.figi variable is not be empty!")
1016
1017        else:
1018            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1019                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1020                raise Exception("Instrument not allowed")
1021
1022            if not self.iList:
1023                self.iList = self.Listing()
1024
1025            for item in self.iList["Shares"].keys():
1026                if self.figi == self.iList["Shares"][item]["figi"]:
1027                    figiJSON = self.iList["Shares"][item]
1028
1029                    if debug:
1030                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1031
1032                    break
1033
1034            if not figiJSON:
1035                for item in self.iList["Currencies"].keys():
1036                    if self.figi == self.iList["Currencies"][item]["figi"]:
1037                        figiJSON = self.iList["Currencies"][item]
1038
1039                        if debug:
1040                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1041
1042                        break
1043
1044            if not figiJSON:
1045                for item in self.iList["Bonds"].keys():
1046                    if self.figi == self.iList["Bonds"][item]["figi"]:
1047                        figiJSON = self.iList["Bonds"][item]
1048
1049                        if debug:
1050                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1051
1052                        break
1053
1054            if not figiJSON:
1055                for item in self.iList["Etfs"].keys():
1056                    if self.figi == self.iList["Etfs"][item]["figi"]:
1057                        figiJSON = self.iList["Etfs"][item]
1058
1059                        if debug:
1060                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1061
1062                        break
1063
1064            if not figiJSON:
1065                for item in self.iList["Futures"].keys():
1066                    if self.figi == self.iList["Futures"][item]["figi"]:
1067                        figiJSON = self.iList["Futures"][item]
1068
1069                        if debug:
1070                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1071
1072                        break
1073
1074        if figiJSON:
1075            self.figi = figiJSON["figi"]
1076            self.ticker = figiJSON["ticker"]
1077
1078            if requestPrice:
1079                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1080
1081                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1082                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1083
1084                else:
1085                    figiJSON["currentPrice"]["changes"] = 0
1086
1087            if show:
1088                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1089
1090        else:
1091            if show:
1092                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1093
1094        return figiJSON
1095
1096    def GetCurrentPrices(self, show: bool = True) -> dict:
1097        """
1098        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1099        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1100
1101        See also:
1102
1103        :param show: if `True` then print DOM to log and console.
1104        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1105        """
1106        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1107
1108        if self.depth < 1:
1109            uLogger.error("Depth of Market (DOM) must be >=1!")
1110            raise Exception("Incorrect value")
1111
1112        if not (self.ticker or self.figi):
1113            uLogger.error("self.ticker or self.figi variables must be defined!")
1114            raise Exception("Ticker or FIGI required")
1115
1116        if self.ticker and not self.figi:
1117            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1118            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1119
1120        if not self.ticker and self.figi:
1121            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1122            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1123
1124        if not self.figi:
1125            uLogger.error("FIGI is not defined!")
1126            raise Exception("Ticker or FIGI required")
1127
1128        else:
1129            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1130
1131            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1132            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1133            self.body = str({"figi": self.figi, "depth": self.depth})
1134            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1135
1136            if pricesResponse:
1137                # list of dicts with sellers orders:
1138                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1139
1140                # list of dicts with buyers orders:
1141                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1142
1143                # max price of instrument at this time:
1144                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1145
1146                # min price of instrument at this time:
1147                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1148
1149                # last price of deal with instrument:
1150                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1151
1152                # last close price of instrument:
1153                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1154
1155            else:
1156                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1157                uLogger.debug("Server response: {}".format(pricesResponse))
1158
1159            if show:
1160                if prices["buy"] or prices["sell"]:
1161                    info = [
1162                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1163                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1164                            self.ticker,
1165                            self.figi,
1166                            self.depth,
1167                        ),
1168                        uLog.sepShort, "\n",
1169                        " Orders of Buyers   | Orders of Sellers\n",
1170                        uLog.sepShort, "\n",
1171                        " Sell prices (vol.) | Buy prices (vol.)\n",
1172                        uLog.sepShort, "\n",
1173                    ]
1174
1175                    if not prices["buy"]:
1176                        info.append("                    | No orders!\n")
1177                        sumBuy = 0
1178
1179                    else:
1180                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1181                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1182                        for item in maxMinSorted:
1183                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1184
1185                    if not prices["sell"]:
1186                        info.append("No orders!          |\n")
1187                        sumSell = 0
1188
1189                    else:
1190                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1191                        for item in prices["sell"]:
1192                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1193
1194                    info.extend([
1195                        uLog.sepShort, "\n",
1196                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1197                        uLog.sepShort, "\n",
1198                    ])
1199
1200                    infoText = "".join(info)
1201
1202                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1203
1204                else:
1205                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1206
1207        return prices
1208
1209    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1210        """
1211        This method get and show information about all available broker instruments for current user account.
1212        If `instrumentsFile` string is not empty then also save information to this file.
1213
1214        :param show: if `True` then print results to console, if `False` - print only to file.
1215        :return: multi-lines string with all available broker instruments
1216        """
1217        if not self.iList:
1218            self.iList = self.Listing()
1219
1220        info = [
1221            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1222            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1223        ]
1224
1225        # add instruments count by type:
1226        for iType in self.iList.keys():
1227            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1228
1229        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1230        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1231
1232        # generating info tables with all instruments by type:
1233        for iType in self.iList.keys():
1234            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1235
1236            for instrument in self.iList[iType].keys():
1237                iName = self.iList[iType][instrument]["name"]  # instrument's name
1238                if len(iName) > 57:
1239                    iName = "{}...".format(iName[:54])  # right trim for a long string
1240
1241                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1242                    self.iList[iType][instrument]["ticker"],
1243                    iName,
1244                    self.iList[iType][instrument]["figi"],
1245                    self.iList[iType][instrument]["currency"],
1246                    self.iList[iType][instrument]["lot"],
1247                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1248                ))
1249
1250        infoText = "".join(info)
1251
1252        if show:
1253            uLogger.info(infoText)
1254
1255        if self.instrumentsFile:
1256            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1257                fH.write(infoText)
1258
1259            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1260
1261        return infoText
1262
1263    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1264        """
1265        This method search and show information about instruments by part of its ticker, FIGI or name.
1266        If `searchResultsFile` string is not empty then also save information to this file.
1267
1268        :param pattern: string with part of ticker, FIGI or instrument's name.
1269        :param show: if `True` then print results to console, if `False` - return list of result only.
1270        :return: list of dictionaries with all found instruments.
1271        """
1272        if not self.iList:
1273            self.iList = self.Listing()
1274
1275        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1276        compiledPattern = re.compile(pattern, re.IGNORECASE)
1277
1278        for iType in self.iList:
1279            for instrument in self.iList[iType].values():
1280                searchResult = compiledPattern.search(" ".join(
1281                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1282                ))
1283
1284                if searchResult:
1285                    searchResults[iType][instrument["ticker"]] = instrument
1286
1287        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1288        info = [
1289            "# Search results\n\n",
1290            "* **Search pattern:** [{}]\n".format(pattern),
1291            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1292            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1293        ]
1294        infoShort = info[:]
1295
1296        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1297        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1298        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1299
1300        if resultsLen == 0:
1301            info.append("\nNo results\n")
1302            infoShort.append("\nNo results\n")
1303            uLogger.warning("No results. Try changing your search pattern.")
1304
1305        else:
1306            for iType in searchResults:
1307                iTypeValuesCount = len(searchResults[iType].values())
1308                if iTypeValuesCount > 0:
1309                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1310                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311
1312                    for instrument in searchResults[iType].values():
1313                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1314                            instrument["type"],
1315                            instrument["ticker"],
1316                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1317                            instrument["figi"],
1318                        ))
1319
1320                    if iTypeValuesCount <= 5:
1321                        infoShort.extend(info[-iTypeValuesCount:])
1322
1323                    else:
1324                        infoShort.extend(info[-5:])
1325                        infoShort.append(skippedLine)
1326
1327        infoText = "".join(info)
1328        infoTextShort = "".join(infoShort)
1329
1330        if show:
1331            uLogger.info(infoTextShort)
1332            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1333
1334        if self.searchResultsFile:
1335            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1336                fH.write(infoText)
1337
1338            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1339
1340        return searchResults
1341
1342    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1343        """
1344        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1345
1346        :param instruments: list of strings with tickers or FIGIs.
1347        :return: list with unique instrument FIGIs only.
1348        """
1349        requestedInstruments = []
1350        for iName in instruments:
1351            if iName not in self.aliases.keys():
1352                if iName not in requestedInstruments:
1353                    requestedInstruments.append(iName)
1354
1355            else:
1356                if iName not in requestedInstruments:
1357                    if self.aliases[iName] not in requestedInstruments:
1358                        requestedInstruments.append(self.aliases[iName])
1359
1360        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1361
1362        onlyUniqueFIGIs = []
1363        for iName in requestedInstruments:
1364            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1365                continue
1366
1367            self.ticker = iName
1368            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1369
1370            if not iData:
1371                self.ticker = ""
1372                self.figi = iName
1373
1374                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1375
1376                if not iData:
1377                    self.figi = ""
1378                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1379
1380            if iData and iData["figi"] not in onlyUniqueFIGIs:
1381                onlyUniqueFIGIs.append(iData["figi"])
1382
1383        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1384
1385        return onlyUniqueFIGIs
1386
1387    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1388        """
1389        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1390        See limits: https://tinkoff.github.io/investAPI/limits/
1391        If `pricesFile` string is not empty then also save information to this file.
1392
1393        :param instruments: list of strings with tickers or FIGIs.
1394        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1395        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1396                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1397        """
1398        if instruments is None or not instruments:
1399            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1400            raise Exception("Ticker or FIGI required")
1401
1402        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1403
1404        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1405
1406        iList = []  # trying to get info and current prices about all unique instruments:
1407        for self.figi in onlyUniqueFIGIs:
1408            iData = self.SearchByFIGI(requestPrice=True)
1409            iList.append(iData)
1410
1411        self.ShowListOfPrices(iList, show)
1412
1413        return iList
1414
1415    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1416        """
1417        Show table contains current prices of given instruments.
1418
1419        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1420                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1421        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1422        :return: multilines text in Markdown format as a table contains current prices.
1423        """
1424        infoText = ""
1425
1426        if show or self.pricesFile:
1427            info = [
1428                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1429                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1430                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1431            ]
1432
1433            for item in iList:
1434                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1435                    item["ticker"],
1436                    item["figi"],
1437                    item["type"],
1438                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1439                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1440                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1441                    "{} / {}".format(
1442                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1443                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1444                    ),
1445                    "{} / {}".format(
1446                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1447                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1448                    ),
1449                    item["currency"],
1450                ))
1451
1452            infoText = "".join(info)
1453
1454            if show:
1455                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1456
1457            if self.pricesFile:
1458                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1459                    fH.write(infoText)
1460
1461                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1462
1463        return infoText
1464
1465    def RequestTradingStatus(self) -> dict:
1466        """
1467        Requesting trading status for the instrument defined by `figi` variable.
1468        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1469        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1470
1471        :return: dictionary with trading status attributes. Response example:
1472                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1473                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1474        """
1475        if self.figi is None or not self.figi:
1476            uLogger.error("Variable `figi` must be defined for using this method!")
1477            raise Exception("FIGI required")
1478
1479        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1480
1481        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1482        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1483        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1484
1485        uLogger.debug("Records about current trading status successfully received")
1486
1487        return tradingStatus
1488
1489    def RequestPortfolio(self) -> dict:
1490        """
1491        Requesting actual user's portfolio for current `accountId`.
1492        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1493        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1494
1495        :return: dictionary with user's portfolio.
1496        """
1497        if self.accountId is None or not self.accountId:
1498            uLogger.error("Variable `accountId` must be defined for using this method!")
1499            raise Exception("Account ID required")
1500
1501        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1502
1503        self.body = str({"accountId": self.accountId})
1504        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1505        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1506
1507        uLogger.debug("Records about user's portfolio successfully received")
1508
1509        return rawPortfolio
1510
1511    def RequestPositions(self) -> dict:
1512        """
1513        Requesting open positions by currencies and instruments for current `accountId`.
1514        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1515        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1516
1517        :return: dictionary with open positions by instruments.
1518        """
1519        if self.accountId is None or not self.accountId:
1520            uLogger.error("Variable `accountId` must be defined for using this method!")
1521            raise Exception("Account ID required")
1522
1523        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1524
1525        self.body = str({"accountId": self.accountId})
1526        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1527        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1528
1529        uLogger.debug("Records about current open positions successfully received")
1530
1531        return rawPositions
1532
1533    def RequestPendingOrders(self) -> list:
1534        """
1535        Requesting current actual pending orders for current `accountId`.
1536        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1537        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1538
1539        :return: list of dictionaries with pending orders.
1540        """
1541        if self.accountId is None or not self.accountId:
1542            uLogger.error("Variable `accountId` must be defined for using this method!")
1543            raise Exception("Account ID required")
1544
1545        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1546
1547        self.body = str({"accountId": self.accountId})
1548        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1549        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1550
1551        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1552
1553        return rawOrders
1554
1555    def RequestStopOrders(self) -> list:
1556        """
1557        Requesting current actual stop orders for current `accountId`.
1558        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1559        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1560
1561        :return: list of dictionaries with stop orders.
1562        """
1563        if self.accountId is None or not self.accountId:
1564            uLogger.error("Variable `accountId` must be defined for using this method!")
1565            raise Exception("Account ID required")
1566
1567        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1568
1569        self.body = str({"accountId": self.accountId})
1570        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1571        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1572
1573        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1574
1575        return rawStopOrders
1576
1577    def Overview(self, show: bool = False, details: str = "full") -> dict:
1578        """
1579        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1580        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1581        are defined then also save information to file.
1582
1583        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1584        many requests about the state of the portfolio, and then, based on the received data, a large number
1585        of calculation and statistics are collected.
1586
1587        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1588        :param details: how detailed should the information be? You should specify one of strings:
1589                        `full` - shows full available information about portfolio status (by default),
1590                        `positions` - shows only open positions,
1591                        `digest` - show a short digest of the portfolio status,
1592                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1593                        `orders` - shows only sections of open limits and stop orders.
1594        :return: dictionary with client's raw portfolio and some statistics.
1595        """
1596        if self.accountId is None or not self.accountId:
1597            uLogger.error("Variable `accountId` must be defined for using this method!")
1598            raise Exception("Account ID required")
1599
1600        view = {
1601            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1602                "headers": {},  # list of dictionaries, response headers without "positions" section
1603                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1604                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1605                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1606                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1607                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1608                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1609                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1610                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1611                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1612            },
1613            "stat": {  # --- some statistics calculated using "raw" sections:
1614                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1615                "availableRUB": 0.,  # available rubles (without other currencies)
1616                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1617                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1618                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1619                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1620                "sharesCostRUB": 0.,  # costs of all shares in RUB
1621                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1622                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1623                "futuresCostRUB": 0.,  # costs of all futures in RUB
1624                "Currencies": [],  # list of dictionaries of all currencies statistics
1625                "Shares": [],  # list of dictionaries of all shares statistics
1626                "Bonds": [],  # list of dictionaries of all bonds statistics
1627                "Etfs": [],  # list of dictionaries of all etfs statistics
1628                "Futures": [],  # list of dictionaries of all futures statistics
1629                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1630                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1631                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1632                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1633                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1634            },
1635            "analytics": {  # --- some analytics of portfolio:
1636                "distrByAssets": {},  # portfolio distribution by assets
1637                "distrByCompanies": {},  # portfolio distribution by companies
1638                "distrBySectors": {},  # portfolio distribution by sectors
1639                "distrByCurrencies": {},  # portfolio distribution by currencies
1640                "distrByCountries": {},  # portfolio distribution by countries
1641            }
1642        }
1643
1644        details = details.lower()
1645        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1646        if details not in availableDetails:
1647            details = "full"
1648            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1649
1650        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1651
1652        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1653        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1654        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1655        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1656
1657        # save response headers without "positions" section:
1658        for key in portfolioResponse.keys():
1659            if key != "positions":
1660                view["raw"]["headers"][key] = portfolioResponse[key]
1661
1662            else:
1663                continue
1664
1665        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1666        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1667        for item in portfolioResponse["positions"]:
1668            if item["instrumentType"] == "currency":
1669                self.figi = item["figi"]
1670                curr = self.SearchByFIGI(requestPrice=False)
1671
1672                # current price of currency in RUB:
1673                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1674                    "name": curr["name"],
1675                    "currentPrice": NanoToFloat(
1676                        item["currentPrice"]["units"],
1677                        item["currentPrice"]["nano"]
1678                    ),
1679                }
1680
1681                view["raw"]["Currencies"].append(item)
1682
1683            elif item["instrumentType"] == "share":
1684                view["raw"]["Shares"].append(item)
1685
1686            elif item["instrumentType"] == "bond":
1687                view["raw"]["Bonds"].append(item)
1688
1689            elif item["instrumentType"] == "etf":
1690                view["raw"]["Etfs"].append(item)
1691
1692            elif item["instrumentType"] == "futures":
1693                view["raw"]["Futures"].append(item)
1694
1695            else:
1696                continue
1697
1698        # how many volume of currencies (by ISO currency name) are blocked:
1699        for item in view["raw"]["positions"]["blocked"]:
1700            blocked = NanoToFloat(item["units"], item["nano"])
1701            if blocked > 0:
1702                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1703
1704        # how many volume of instruments (by FIGI) are blocked:
1705        for item in view["raw"]["positions"]["securities"]:
1706            blocked = int(item["blocked"])
1707            if blocked > 0:
1708                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1709
1710        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1711
1712        if "rub" in allBlocked.keys():
1713            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1714
1715        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1716        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1717        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1718        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1719        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1720        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1721        view["stat"]["portfolioCostRUB"] = sum([
1722            view["stat"]["allCurrenciesCostRUB"],
1723            view["stat"]["sharesCostRUB"],
1724            view["stat"]["bondsCostRUB"],
1725            view["stat"]["etfsCostRUB"],
1726            view["stat"]["futuresCostRUB"],
1727        ])
1728
1729        # --- calculating some portfolio statistics:
1730        byComp = {}  # distribution by companies
1731        bySect = {}  # distribution by sectors
1732        byCurr = {}  # distribution by currencies (include RUB)
1733        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1734        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1735
1736        for item in portfolioResponse["positions"]:
1737            self.figi = item["figi"]
1738            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1739
1740            if instrument:
1741                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1742                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1743
1744                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1745                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1746
1747                else:
1748                    blocked = 0
1749
1750                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1751                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1752                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1753                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1754                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1755                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1756                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1757                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1758                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1759                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1760                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1761                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1762
1763                statData = {
1764                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1765                    "ticker": instrument["ticker"],  # ticker by FIGI
1766                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1767                    "volume": volume,  # available volume of instrument
1768                    "lots": lots,  # volume in lots of instrument
1769                    "direction": direction,  # direction of an instrument's position: short or long
1770                    "blocked": blocked,  # blocked volume of currency or instrument
1771                    "currentPrice": curPrice,  # current instrument's price in basic asset
1772                    "average": average,  # current average position price
1773                    "cost": cost,  # current cost of all volume of instrument in basic asset
1774                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1775                    "costRUB": costRUB,  # cost of instrument in ruble
1776                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1777                    "profit": profit,  # expected profit at current moment
1778                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1779                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1780                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1781                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1782                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1783                    "step": instrument["step"],  # minimum price increment
1784                }
1785
1786                # adding distribution by unique countries:
1787                if statData["country"] not in byCountry.keys():
1788                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1789
1790                else:
1791                    byCountry[statData["country"]]["cost"] += costRUB
1792                    byCountry[statData["country"]]["percent"] += percentCostRUB
1793
1794                if item["instrumentType"] != "currency":
1795                    # adding distribution by unique companies:
1796                    if statData["name"]:
1797                        if statData["name"] not in byComp.keys():
1798                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1799
1800                        else:
1801                            byComp[statData["name"]]["cost"] += costRUB
1802                            byComp[statData["name"]]["percent"] += percentCostRUB
1803
1804                    # adding distribution by unique sectors:
1805                    if statData["sector"] not in bySect.keys():
1806                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1807
1808                    else:
1809                        bySect[statData["sector"]]["cost"] += costRUB
1810                        bySect[statData["sector"]]["percent"] += percentCostRUB
1811
1812                # adding distribution by unique currencies:
1813                if currency not in byCurr.keys():
1814                    byCurr[currency] = {
1815                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1816                        "cost": costRUB,
1817                        "percent": percentCostRUB
1818                    }
1819
1820                else:
1821                    byCurr[currency]["cost"] += costRUB
1822                    byCurr[currency]["percent"] += percentCostRUB
1823
1824                # saving statistics for every instrument:
1825                if item["instrumentType"] == "currency":
1826                    view["stat"]["Currencies"].append(statData)
1827
1828                    # update dict with free funds for trading (total - blocked) by currencies
1829                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1830                    view["stat"]["funds"][currency] = {
1831                        "total": volume,
1832                        "totalCostRUB": costRUB,  # total volume cost in rubles
1833                        "free": volume - blocked,
1834                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1835                    }
1836
1837                elif item["instrumentType"] == "share":
1838                    view["stat"]["Shares"].append(statData)
1839
1840                elif item["instrumentType"] == "bond":
1841                    view["stat"]["Bonds"].append(statData)
1842
1843                elif item["instrumentType"] == "etf":
1844                    view["stat"]["Etfs"].append(statData)
1845
1846                elif item["instrumentType"] == "Futures":
1847                    view["stat"]["Futures"].append(statData)
1848
1849                else:
1850                    continue
1851
1852        # total changes in Russian Ruble:
1853        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1854        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1855        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1856        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1857        view["stat"]["funds"]["rub"] = {
1858            "total": view["stat"]["availableRUB"],
1859            "totalCostRUB": view["stat"]["availableRUB"],
1860            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1861            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1862        }
1863
1864        # --- pending orders sector data:
1865        uniquePendingOrders = []
1866        uniquePendingOrdersFIGIs = []
1867        for item in view["raw"]["orders"]:
1868            if item["figi"] not in uniquePendingOrdersFIGIs:
1869                uniquePendingOrdersFIGIs.append(item["figi"])
1870                uniquePendingOrders.append(item)
1871
1872        for item in uniquePendingOrders:
1873            self.figi = item["figi"]
1874            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1875
1876            if instrument:
1877                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1878                orderType = TKS_ORDER_TYPES[item["orderType"]]
1879                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1880                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1881
1882                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1883                if item["direction"] == "ORDER_DIRECTION_BUY":
1884                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1885
1886                else:
1887                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1888
1889                # requested price for order execution:
1890                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1891
1892                # necessary changes in percent to reach target from current price:
1893                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1894
1895                view["stat"]["orders"].append({
1896                    "orderID": item["orderId"],  # orderId number parameter of current order
1897                    "figi": item["figi"],  # FIGI identification
1898                    "ticker": instrument["ticker"],  # ticker name by FIGI
1899                    "lotsRequested": item["lotsRequested"],  # requested lots value
1900                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1901                    "currentPrice": lastPrice,  # current instrument's price for defined action
1902                    "targetPrice": target,  # requested price for order execution in base currency
1903                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1904                    "percentChanges": changes,  # changes in percent to target from current price
1905                    "currency": item["currency"],  # instrument's currency name
1906                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1907                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1908                    "status": orderState,  # order status from TKS_ORDER_STATES
1909                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1910                })
1911
1912        # --- stop orders sector data:
1913        uniqueStopOrders = []
1914        uniqueStopOrdersFIGIs = []
1915        for item in view["raw"]["stopOrders"]:
1916            if item["figi"] not in uniqueStopOrdersFIGIs:
1917                uniqueStopOrdersFIGIs.append(item["figi"])
1918                uniqueStopOrders.append(item)
1919
1920        for item in uniqueStopOrders:
1921            self.figi = item["figi"]
1922            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1923
1924            if instrument:
1925                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1926                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1927                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1928
1929                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1930                if "expirationTime" in item.keys():
1931                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1932                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1933
1934                else:
1935                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1936                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1937
1938                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1939                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1940                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1941
1942                else:
1943                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1944
1945                # requested price when stop-order executed:
1946                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1947
1948                # price for limit-order, set up when stop-order executed:
1949                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1950
1951                # necessary changes in percent to reach target from current price:
1952                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1953
1954                view["stat"]["stopOrders"].append({
1955                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1956                    "figi": item["figi"],  # FIGI identification
1957                    "ticker": instrument["ticker"],  # ticker name by FIGI
1958                    "lotsRequested": item["lotsRequested"],  # requested lots value
1959                    "currentPrice": lastPrice,  # current instrument's price for defined action
1960                    "targetPrice": target,  # requested price for stop-order execution in base currency
1961                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1962                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1963                    "percentChanges": changes,  # changes in percent to target from current price
1964                    "currency": item["currency"],  # instrument's currency name
1965                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1966                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1967                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1968                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1969                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1970                })
1971
1972        # --- calculating data for analytics section:
1973        # portfolio distribution by assets:
1974        view["analytics"]["distrByAssets"] = {
1975            "Ruble": {
1976                "uniques": 1,
1977                "cost": view["stat"]["availableRUB"],
1978                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1979            },
1980            "Currencies": {
1981                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1982                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1983                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1984            },
1985            "Shares": {
1986                "uniques": len(view["stat"]["Shares"]),
1987                "cost": view["stat"]["sharesCostRUB"],
1988                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1989            },
1990            "Bonds": {
1991                "uniques": len(view["stat"]["Bonds"]),
1992                "cost": view["stat"]["bondsCostRUB"],
1993                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1994            },
1995            "Etfs": {
1996                "uniques": len(view["stat"]["Etfs"]),
1997                "cost": view["stat"]["etfsCostRUB"],
1998                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1999            },
2000            "Futures": {
2001                "uniques": len(view["stat"]["Futures"]),
2002                "cost": view["stat"]["futuresCostRUB"],
2003                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2004            },
2005        }
2006
2007        # portfolio distribution by companies:
2008        view["analytics"]["distrByCompanies"]["All money cash"] = {
2009            "ticker": "",
2010            "cost": view["stat"]["allCurrenciesCostRUB"],
2011            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2012        }
2013        view["analytics"]["distrByCompanies"].update(byComp)
2014
2015        # portfolio distribution by sectors:
2016        view["analytics"]["distrBySectors"]["All money cash"] = {
2017            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2018            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2019        }
2020        view["analytics"]["distrBySectors"].update(bySect)
2021
2022        # portfolio distribution by currencies:
2023        view["analytics"]["distrByCurrencies"].update(byCurr)
2024        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2025        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2026
2027        # portfolio distribution by countries:
2028        view["analytics"]["distrByCountries"].update(byCountry)
2029
2030        # --- Prepare text statistics overview in human-readable:
2031        if show:
2032            # Whatever the value `details`, header not changes:
2033            info = [
2034                "# Client's portfolio\n\n",
2035                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2036                "* **Account ID:** [{}]\n".format(self.accountId),
2037            ]
2038
2039            if details in ["full", "positions", "digest"]:
2040                info.extend([
2041                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2042                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2043                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2044                        view["stat"]["totalChangesRUB"],
2045                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2046                        view["stat"]["totalChangesPercentRUB"],
2047                    ),
2048                ])
2049
2050            if details in ["full", "positions"]:
2051                info.extend([
2052                    "## Open positions\n\n",
2053                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2054                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2055                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2056                        "{:.2f} ({:.2f}) rub".format(
2057                            view["stat"]["availableRUB"],
2058                            view["stat"]["blockedRUB"],
2059                        )
2060                    )
2061                ])
2062
2063                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2064                    return [
2065                        "|                             |                                 |          |              |              |                     |                              |\n",
2066                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2067                            noTradeStr if noTradeStr else typeStr,
2068                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2069                        ),
2070                    ]
2071
2072                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2073                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2074                        "{} [{}]".format(data["ticker"], data["figi"]),
2075                        "{:.2f} ({:.2f}) {}".format(
2076                            data["volume"],
2077                            data["blocked"],
2078                            data["currency"],
2079                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2080                            data["volume"],
2081                            data["blocked"],
2082                        ),
2083                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2084                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2085                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2086                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2087                        "{}{:.2f} {} ({}{:.2f}%)".format(
2088                            "+" if data["profit"] > 0 else "",
2089                            data["profit"], data["baseCurrencyName"],
2090                            "+" if data["percentProfit"] > 0 else "",
2091                            data["percentProfit"],
2092                        ),
2093                    )
2094
2095                # --- Show currencies section:
2096                if view["stat"]["Currencies"]:
2097                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2098                    for item in view["stat"]["Currencies"]:
2099                        info.append(_InfoStr(item, showCurrencyName=True))
2100
2101                else:
2102                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2103
2104                # --- Show shares section:
2105                if view["stat"]["Shares"]:
2106                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2107
2108                    for item in view["stat"]["Shares"]:
2109                        info.append(_InfoStr(item))
2110
2111                else:
2112                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2113
2114                # --- Show bonds section:
2115                if view["stat"]["Bonds"]:
2116                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2117
2118                    for item in view["stat"]["Bonds"]:
2119                        info.append(_InfoStr(item))
2120
2121                else:
2122                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2123
2124                # --- Show etfs section:
2125                if view["stat"]["Etfs"]:
2126                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2127
2128                    for item in view["stat"]["Etfs"]:
2129                        info.append(_InfoStr(item))
2130
2131                else:
2132                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2133
2134                # --- Show futures section:
2135                if view["stat"]["Futures"]:
2136                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2137
2138                    for item in view["stat"]["Futures"]:
2139                        info.append(_InfoStr(item))
2140
2141                else:
2142                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2143
2144            if details in ["full", "orders"]:
2145                # --- Show pending orders section:
2146                if view["stat"]["orders"]:
2147                    info.extend([
2148                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2149                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2150                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2151                    ])
2152
2153                    for item in view["stat"]["orders"]:
2154                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2155                            "{} [{}]".format(item["ticker"], item["figi"]),
2156                            item["orderID"],
2157                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2158                            "{} {} ({}{:.2f}%)".format(
2159                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2160                                item["baseCurrencyName"],
2161                                "+" if item["percentChanges"] > 0 else "",
2162                                float(item["percentChanges"]),
2163                            ),
2164                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2165                            item["action"],
2166                            item["type"],
2167                            item["date"],
2168                        ))
2169
2170                else:
2171                    info.append("\n## Total pending limit-orders: 0\n")
2172
2173                # --- Show stop orders section:
2174                if view["stat"]["stopOrders"]:
2175                    info.extend([
2176                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2177                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2178                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2179                    ])
2180
2181                    for item in view["stat"]["stopOrders"]:
2182                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2183                            "{} [{}]".format(item["ticker"], item["figi"]),
2184                            item["orderID"],
2185                            item["lotsRequested"],
2186                            "{} {} ({}{:.2f}%)".format(
2187                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2188                                item["baseCurrencyName"],
2189                                "+" if item["percentChanges"] > 0 else "",
2190                                float(item["percentChanges"]),
2191                            ),
2192                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2193                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2194                            item["action"],
2195                            item["type"],
2196                            item["expType"],
2197                            item["createDate"],
2198                            item["expDate"],
2199                        ))
2200
2201                else:
2202                    info.append("\n## Total stop-orders: 0\n")
2203
2204            if details in ["full", "analytics"]:
2205                # -- Show analytics section:
2206                if view["stat"]["portfolioCostRUB"] > 0:
2207                    info.extend([
2208                        "\n# Analytics\n"
2209                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2210                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2211                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2212                            view["stat"]["totalChangesRUB"],
2213                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2214                            view["stat"]["totalChangesPercentRUB"],
2215                        ),
2216                        "\n## Portfolio distribution by assets\n"
2217                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2218                        "|------------|---------|---------|--------------------|\n",
2219                    ])
2220
2221                    for key in view["analytics"]["distrByAssets"].keys():
2222                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2223                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2224                                key,
2225                                view["analytics"]["distrByAssets"][key]["uniques"],
2226                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2227                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2228                            ))
2229
2230                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2231                    info.extend([
2232                        "\n## Portfolio distribution by companies\n"
2233                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2234                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2235                    ])
2236
2237                    for company in view["analytics"]["distrByCompanies"].keys():
2238                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2239                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2240                            info.append("| {} | {:<7} | {:<18} |\n".format(
2241                                "{}{}{}".format(
2242                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2243                                    company,
2244                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2245                                ),
2246                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2247                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2248                            ))
2249
2250                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2251                    info.extend([
2252                        "\n## Portfolio distribution by sectors\n"
2253                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2254                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2255                    ])
2256
2257                    for sector in view["analytics"]["distrBySectors"].keys():
2258                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2259                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2260                                sector,
2261                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2262                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2263                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2264                            ))
2265
2266                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2267                    info.extend([
2268                        "\n## Portfolio distribution by currencies\n"
2269                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2270                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2271                    ])
2272
2273                    for curr in view["analytics"]["distrByCurrencies"].keys():
2274                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2275                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2276                            info.append("| {} | {:<7} | {:<18} |\n".format(
2277                                "[{}] {}{}".format(
2278                                    curr,
2279                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2280                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2281                                ),
2282                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2283                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2284                            ))
2285
2286                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2287                    info.extend([
2288                        "\n## Portfolio distribution by countries\n"
2289                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2290                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2291                    ])
2292
2293                    for country in view["analytics"]["distrByCountries"].keys():
2294                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2295                            nameLen = len(country)
2296                            info.append("| {} | {:<7} | {:<18} |\n".format(
2297                                "{}{}".format(
2298                                    country,
2299                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2300                                ),
2301                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2302                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2303                            ))
2304
2305            infoText = "".join(info)
2306
2307            uLogger.info(infoText)
2308
2309            if details == "full" and self.overviewFile:
2310                filename = self.overviewFile
2311
2312            elif details == "digest" and self.overviewDigestFile:
2313                filename = self.overviewDigestFile
2314
2315            elif details == "positions" and self.overviewPositionsFile:
2316                filename = self.overviewPositionsFile
2317
2318            elif details == "orders" and self.overviewOrdersFile:
2319                filename = self.overviewOrdersFile
2320
2321            elif details == "analytics" and self.overviewAnalyticsFile:
2322                filename = self.overviewAnalyticsFile
2323
2324            else:
2325                filename = ""
2326
2327            if filename:
2328                with open(filename, "w", encoding="UTF-8") as fH:
2329                    fH.write(infoText)
2330
2331                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2332
2333        return view
2334
2335    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2336        """
2337        Returns history operations between two given dates for current `accountId`.
2338        If `reportFile` string is not empty then also save human-readable report.
2339        Shows some statistical data of closed positions.
2340
2341        :param start: see docstring in `GetDatesAsString()` method
2342        :param end: see docstring in `GetDatesAsString()` method
2343        :param show: if `True` then also prints all records to the console.
2344        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2345        :return: original list of dictionaries with history of deals records from API ("operations" key):
2346                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2347                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2348        """
2349        if self.accountId is None or not self.accountId:
2350            uLogger.error("Variable `accountId` must be defined for using this method!")
2351            raise Exception("Account ID required")
2352
2353        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2354
2355        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2356
2357        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2358        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2359        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2360        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2361        customStat = {}  # custom statistics in additional to responseJSON
2362
2363        # --- output report in human-readable format:
2364        if show or self.reportFile:
2365            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2366            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2367            nextDay = ""
2368
2369            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2370
2371            if len(ops) > 0:
2372                customStat = {
2373                    "opsCount": 0,  # total operations count
2374                    "buyCount": 0,  # buy operations
2375                    "sellCount": 0,  # sell operations
2376                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2377                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2378                    "payIn": {"rub": 0.},  # Deposit brokerage account
2379                    "payOut": {"rub": 0.},  # Withdrawals
2380                    "divs": {"rub": 0.},  # Dividends income
2381                    "coupons": {"rub": 0.},  # Coupon's income
2382                    "brokerCom": {"rub": 0.},  # Service commissions
2383                    "serviceCom": {"rub": 0.},  # Service commissions
2384                    "marginCom": {"rub": 0.},  # Margin commissions
2385                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2386                }
2387
2388                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2389                for item in ops:
2390                    if item["state"] == "OPERATION_STATE_EXECUTED":
2391                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2392
2393                        # count buy operations:
2394                        if "_BUY" in item["operationType"]:
2395                            customStat["buyCount"] += 1
2396
2397                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2398                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2399
2400                            else:
2401                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2402
2403                        # count sell operations:
2404                        elif "_SELL" in item["operationType"]:
2405                            customStat["sellCount"] += 1
2406
2407                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2408                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2409
2410                            else:
2411                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2412
2413                        # count incoming operations:
2414                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2415                            if item["payment"]["currency"] in customStat["payIn"].keys():
2416                                customStat["payIn"][item["payment"]["currency"]] += payment
2417
2418                            else:
2419                                customStat["payIn"][item["payment"]["currency"]] = payment
2420
2421                        # count withdrawals operations:
2422                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2423                            if item["payment"]["currency"] in customStat["payOut"].keys():
2424                                customStat["payOut"][item["payment"]["currency"]] += payment
2425
2426                            else:
2427                                customStat["payOut"][item["payment"]["currency"]] = payment
2428
2429                        # count dividends income:
2430                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2431                            if item["payment"]["currency"] in customStat["divs"].keys():
2432                                customStat["divs"][item["payment"]["currency"]] += payment
2433
2434                            else:
2435                                customStat["divs"][item["payment"]["currency"]] = payment
2436
2437                        # count coupon's income:
2438                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2439                            if item["payment"]["currency"] in customStat["coupons"].keys():
2440                                customStat["coupons"][item["payment"]["currency"]] += payment
2441
2442                            else:
2443                                customStat["coupons"][item["payment"]["currency"]] = payment
2444
2445                        # count broker commissions:
2446                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2447                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2448                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2449
2450                            else:
2451                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2452
2453                        # count service commissions:
2454                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2455                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2456                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2457
2458                            else:
2459                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2460
2461                        # count margin commissions:
2462                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2463                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2464                                customStat["marginCom"][item["payment"]["currency"]] += payment
2465
2466                            else:
2467                                customStat["marginCom"][item["payment"]["currency"]] = payment
2468
2469                        # count withholding taxes:
2470                        elif "_TAX" in item["operationType"]:
2471                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2472                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2473
2474                            else:
2475                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2476
2477                        else:
2478                            continue
2479
2480                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2481
2482                # --- view "Actions" lines:
2483                info.extend([
2484                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2485                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2486                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2487                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2488                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2489                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2490                    ),
2491                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2492                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2493                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2494                    ),
2495                ])
2496
2497                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2498                for key in opsKeys:
2499                    if key == "rub":
2500                        continue
2501
2502                    info.extend([
2503                        "|                            |                               | {:<28} |                      |                        |\n".format(
2504                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2505                        ),
2506                        "|                            |                               | {:<28} |                      |                        |\n".format(
2507                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2508                        ),
2509                    ])
2510
2511                info.append(splitLine1)
2512
2513                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2514                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2515                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2516                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2517                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2518                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2519                    )
2520
2521                # --- view "Payments" lines:
2522                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2523                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2524
2525                for key in paymentsKeys:
2526                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2527
2528                info.append(splitLine1)
2529
2530                # --- view "Commissions and taxes" lines:
2531                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2532                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2533
2534                for key in comKeys:
2535                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2536
2537                info.append(splitLine1)
2538
2539                info.extend([
2540                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2541                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2542                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2543                ])
2544
2545            else:
2546                info.append("Broker returned no operations during this period\n")
2547
2548            # --- view "Operations" section:
2549            for item in ops:
2550                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2551                    continue
2552
2553                else:
2554                    self.figi = item["figi"] if item["figi"] else ""
2555                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2556                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2557
2558                    # group of deals during one day:
2559                    if nextDay and item["date"].split("T")[0] != nextDay:
2560                        info.append(splitLine2)
2561                        nextDay = ""
2562
2563                    else:
2564                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2565
2566                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2567                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2568                        self.figi if self.figi else "—",
2569                        instrument["ticker"] if instrument else "—",
2570                        instrument["type"] if instrument else "—",
2571                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2572                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2573                        TKS_OPERATION_STATES[item["state"]],
2574                        TKS_OPERATION_TYPES[item["operationType"]],
2575                    ))
2576
2577            infoText = "".join(info)
2578
2579            if show:
2580                uLogger.info(infoText)
2581
2582            if self.reportFile:
2583                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2584                    fH.write(infoText)
2585
2586                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2587
2588        return ops, customStat
2589
2590    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2591        """
2592        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2593
2594        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2595        Warning! Broker server used ISO UTC time by default.
2596
2597        If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe.
2598        Also, `historyFile` used to update history with `onlyMissing` parameter.
2599
2600        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2601
2602        :param start: see docstring in `GetDatesAsString()` method.
2603        :param end: see docstring in `GetDatesAsString()` method.
2604        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2605                         `"hour"`, `"day"`. Default: `"hour"`.
2606        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2607                            False by default. Warning! History appends only from last candle to current time
2608                            with always update last candle!
2609        :param csvSep: separator if csv-file is used, `,` by default.
2610        :param show: if `True` then also prints pandas dataframe to the console.
2611        :return: pandas dataframe with prices history. Headers of columns are defined by default:
2612                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2613        """
2614        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2615        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2616        history = None  # empty pandas object for history
2617
2618        if interval not in TKS_CANDLE_INTERVALS.keys():
2619            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2620            raise Exception("Incorrect value")
2621
2622        if not (self.ticker or self.figi):
2623            uLogger.error("Ticker or FIGI must be defined!")
2624            raise Exception("Ticker or FIGI required")
2625
2626        if self.ticker and not self.figi:
2627            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2628            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2629
2630        if self.figi and not self.ticker:
2631            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2632            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2633
2634        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2635        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2636        if interval.lower() != "day":
2637            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2638
2639        delta = dtEnd - dtStart  # current UTC time minus last time in file
2640        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2641
2642        # calculate history length in candles:
2643        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2644        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2645            length += 1  # to avoid fraction time
2646
2647        # calculate data blocks count:
2648        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2649
2650        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2651        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2652        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2653        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2654        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2655
2656        tempOld = None  # pandas object for old history, if --only-missing key present
2657        lastTime = None  # datetime object of last old candle in file
2658
2659        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2660            uLogger.debug("--only-missing key present, add only last missing candles...")
2661            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2662
2663            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2664
2665            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2666            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2667            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2668            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2669
2670            # get last datetime object from last string in file or minus 1 delta if file is empty:
2671            if len(tempOld) > 0:
2672                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2673
2674            else:
2675                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2676
2677            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2678
2679        responseJSONs = []  # raw history blocks of data
2680
2681        blockEnd = dtEnd
2682        for item in range(blocks):
2683            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2684            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2685
2686            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2687                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2688            ))
2689
2690            if blockStart == blockEnd:
2691                uLogger.debug("Skipped this zero-length block...")
2692
2693            else:
2694                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2695                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2696                self.body = str({
2697                    "figi": self.figi,
2698                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2699                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2700                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2701                })
2702                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2703
2704                if "code" in responseJSON.keys():
2705                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2706
2707                else:
2708                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2709                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2710
2711                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2712
2713            blockEnd = blockStart
2714
2715        printCount = len(responseJSONs)  # candles to show in console
2716        if responseJSONs:
2717            tempHistory = pd.DataFrame(
2718                data={
2719                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2720                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2721                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2722                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2723                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2724                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2725                    "volume": [int(item["volume"]) for item in responseJSONs],
2726                },
2727                index=range(len(responseJSONs)),
2728                columns=["date", "time", "open", "high", "low", "close", "volume"],
2729            )
2730            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2731            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2732
2733            # append only newest candles to old history if --only-missing key present:
2734            if onlyMissing and tempOld is not None and lastTime is not None:
2735                index = 0  # find start index in tempHistory data:
2736
2737                for i, item in tempHistory.iterrows():
2738                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2739
2740                    if curTime == lastTime:
2741                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2742                        index = i
2743                        printCount = index + 1
2744                        break
2745
2746                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2747
2748            else:
2749                history = tempHistory  # if no `--only-missing` key then load full data from server
2750
2751            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2752
2753        if history is not None and not history.empty:
2754            if show:
2755                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2756                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2757                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2758                ))
2759
2760        else:
2761            uLogger.warning("Received an empty candles history!")
2762
2763        if self.historyFile is not None:
2764            if history is not None and not history.empty:
2765                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2766                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2767
2768            else:
2769                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2770
2771        else:
2772            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.")
2773
2774        return history
2775
2776    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2777        """
2778        Load candles history from csv-file and return pandas dataframe object.
2779
2780        See also: `History()` and `ShowHistoryChart()` methods.
2781
2782        :param filePath: path to csv-file to open.
2783        """
2784        loadedHistory = None  # init candles data object
2785
2786        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2787
2788        if os.path.exists(filePath):
2789            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as pandas dataframe
2790
2791            tfStr = self.priceModel.FormattedDelta(
2792                self.priceModel.timeframe,
2793                "{days} days {hours}h {minutes}m {seconds}s",
2794            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2795                self.priceModel.timeframe,
2796                "{hours}h {minutes}m {seconds}s",
2797            )
2798
2799            if loadedHistory is not None and not loadedHistory.empty:
2800                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2801                    len(loadedHistory),
2802                    tfStr,
2803                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2804                )
2805
2806            else:
2807                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2808
2809        else:
2810            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2811
2812        return loadedHistory
2813
2814    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2815        """
2816        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2817
2818        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2819        Default: `index.html` (both for interact and non-interact candlesticks chart).
2820
2821        See also: `History()` and `LoadHistory()` methods.
2822
2823        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2824        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2825                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2826                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2827                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2828        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2829                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2830        """
2831        if isinstance(candles, str):
2832            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2833            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2834
2835        elif isinstance(candles, pd.DataFrame):
2836            self.priceModel.prices = candles  # set candles chain from variable
2837            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2838
2839            if "datetime" not in candles.columns:
2840                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2841
2842        else:
2843            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2844            raise Exception("Incorrect value")
2845
2846        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2847
2848        if interact:
2849            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2850
2851            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2852
2853        else:
2854            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2855
2856            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2857
2858        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2859
2860    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2861        """
2862        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2863        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2864
2865        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2866
2867        :param operation: string "Buy" or "Sell".
2868        :param lots: volume, integer count of lots >= 1.
2869        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2870        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2871        :param expDate: string "Undefined" by default or local date in future,
2872                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2873        :return: JSON with response from broker server.
2874        """
2875        if self.accountId is None or not self.accountId:
2876            uLogger.error("Variable `accountId` must be defined for using this method!")
2877            raise Exception("Account ID required")
2878
2879        if operation is None or not operation or operation not in ("Buy", "Sell"):
2880            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2881            raise Exception("Incorrect value")
2882
2883        if lots is None or lots < 1:
2884            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2885            lots = 1
2886
2887        if tp is None or tp < 0:
2888            tp = 0
2889
2890        if sl is None or sl < 0:
2891            sl = 0
2892
2893        if expDate is None or not expDate:
2894            expDate = "Undefined"
2895
2896        if not (self.ticker or self.figi):
2897            uLogger.error("Ticker or FIGI must be defined!")
2898            raise Exception("Ticker or FIGI required")
2899
2900        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2901        self.ticker = instrument["ticker"]
2902        self.figi = instrument["figi"]
2903
2904        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2905
2906        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2907        self.body = str({
2908            "figi": self.figi,
2909            "quantity": str(lots),
2910            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2911            "accountId": str(self.accountId),
2912            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2913        })
2914        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2915
2916        if "orderId" in response.keys():
2917            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2918                operation, response["orderId"],
2919                self.ticker, self.figi, lots,
2920                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2921                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2922                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2923            ))
2924
2925        else:
2926            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2927
2928        if tp > 0:
2929            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2930
2931        if sl > 0:
2932            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2933
2934        return response
2935
2936    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2937        """
2938        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2939        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2940
2941        See also: `Order()` and `Trade()` docstrings.
2942
2943        :param lots: volume, integer count of lots >= 1.
2944        :param tp: float > 0, take profit price of stop-order.
2945        :param sl: float > 0, stop loss price of stop-order.
2946        :param expDate: it's a local date in future.
2947                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2948        :return: JSON with response from broker server.
2949        """
2950        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2951
2952    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2953        """
2954        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2955        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2956
2957        See also: `Order()` and `Trade()` docstrings.
2958
2959        :param lots: volume, integer count of lots >= 1.
2960        :param tp: float > 0, take profit price of stop-order.
2961        :param sl: float > 0, stop loss price of stop-order.
2962        :param expDate: it's a local date in the future.
2963                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2964        :return: JSON with response from broker server.
2965        """
2966        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2967
2968    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2969        """
2970        Close position of given instruments.
2971
2972        :param tickers: tickers list of instruments that must be closed.
2973        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2974                         This avoids unnecessary downloading data from the server.
2975        """
2976        if not tickers:
2977            uLogger.info("Tickers list is empty, nothing to close.")
2978
2979        else:
2980            if portfolio is None or not portfolio:
2981                portfolio = self.Overview(show=False)
2982
2983            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2984            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
2985
2986            for ticker in tickers:
2987                if ticker not in allOpenedTickers:
2988                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
2989                    continue
2990
2991                # search open trade info about instrument by ticker:
2992                instrument = {}
2993                for iType in TKS_INSTRUMENTS:
2994                    if instrument:
2995                        break
2996
2997                    for item in portfolio["stat"][iType]:
2998                        if item["ticker"] == ticker:
2999                            instrument = item
3000                            break
3001
3002                if instrument:
3003                    self.ticker = ticker
3004                    self.figi = instrument["figi"]
3005
3006                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3007                        self.ticker,
3008                        self.figi,
3009                        int(instrument["volume"]),
3010                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3011                    ))
3012
3013                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3014
3015                    if tradeLots > 0:
3016                        if instrument["blocked"] > 0:
3017                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3018                                instrument["blocked"],
3019                                self.ticker,
3020                                tradeLots,
3021                            ))
3022
3023                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3024                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3025
3026                    else:
3027                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3028
3029    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3030        """
3031        Close all positions of given instruments with defined type.
3032
3033        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3034        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3035                         This avoids unnecessary downloading data from the server.
3036        """
3037        if iType not in TKS_INSTRUMENTS:
3038            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3039
3040        else:
3041            if portfolio is None or not portfolio:
3042                portfolio = self.Overview(show=False)
3043
3044            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3045            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3046
3047            if tickers and portfolio:
3048                self.CloseTrades(tickers, portfolio)
3049
3050            else:
3051                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3052
3053    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3054        """
3055        Universal method to create market or limit orders with all available parameters for current `accountId`.
3056        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3057
3058        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3059        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3060
3061        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3062        then broker immediately open market order as you can do simple --buy or --sell operations!
3063
3064        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3065        When current price will go up or down to target price value then broker opens a limit order.
3066        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3067
3068        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3069
3070        :param operation: string "Buy" or "Sell".
3071        :param orderType: string "Limit" or "Stop".
3072        :param lots: volume, integer count of lots >= 1.
3073        :param targetPrice: target price > 0. This is open trade price for limit order.
3074        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3075                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3076        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3077                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3078                         Stop loss order always executed by market price.
3079        :param expDate: string "Undefined" by default or local date in future.
3080                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3081                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3082                        A limit order has no expiration date, it lasts until the end of the trading day.
3083        :return: JSON with response from broker server.
3084        """
3085        if self.accountId is None or not self.accountId:
3086            uLogger.error("Variable `accountId` must be defined for using this method!")
3087            raise Exception("Account ID required")
3088
3089        if operation is None or not operation or operation not in ("Buy", "Sell"):
3090            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3091            raise Exception("Incorrect value")
3092
3093        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3094            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3095            raise Exception("Incorrect value")
3096
3097        if lots is None or lots < 1:
3098            uLogger.error("You must define trade volume > 0: integer count of lots!")
3099            raise Exception("Incorrect value")
3100
3101        if targetPrice is None or targetPrice <= 0:
3102            uLogger.error("Target price for limit-order must be greater than 0!")
3103            raise Exception("Incorrect value")
3104
3105        if limitPrice is None or limitPrice <= 0:
3106            limitPrice = targetPrice
3107
3108        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3109            stopType = "Limit"
3110
3111        if expDate is None or not expDate:
3112            expDate = "Undefined"
3113
3114        if not (self.ticker or self.figi):
3115            uLogger.error("Tocker or FIGI must be defined!")
3116            raise Exception("Ticker or FIGI required")
3117
3118        response = {}
3119        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3120        self.ticker = instrument["ticker"]
3121        self.figi = instrument["figi"]
3122
3123        if orderType == "Limit":
3124            uLogger.debug(
3125                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3126                    self.ticker, self.figi,
3127                    operation, lots, targetPrice, instrument["currency"],
3128                ))
3129
3130            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3131            self.body = str({
3132                "figi": self.figi,
3133                "quantity": str(lots),
3134                "price": FloatToNano(targetPrice),
3135                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3136                "accountId": str(self.accountId),
3137                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3138            })
3139            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3140
3141            if "orderId" in response.keys():
3142                uLogger.info(
3143                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3144                        response["orderId"],
3145                        self.ticker, self.figi,
3146                        operation, lots, targetPrice, instrument["currency"],
3147                    ))
3148
3149                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3150                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3151                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3152                            targetPrice, instrument["currency"],
3153                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3154                        ))
3155
3156                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3157                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3158                            targetPrice, instrument["currency"],
3159                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3160                        ))
3161
3162            else:
3163                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3164
3165        if orderType == "Stop":
3166            uLogger.debug(
3167                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3168                    self.ticker, self.figi,
3169                    operation, lots,
3170                    targetPrice, instrument["currency"],
3171                    limitPrice, instrument["currency"],
3172                    stopType, expDate,
3173                ))
3174
3175            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3176            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3177            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3178
3179            body = {
3180                "figi": self.figi,
3181                "quantity": str(lots),
3182                "price": FloatToNano(limitPrice),
3183                "stopPrice": FloatToNano(targetPrice),
3184                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3185                "accountId": str(self.accountId),
3186                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3187                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3188            }
3189
3190            if expDateUTC:
3191                body["expireDate"] = expDateUTC
3192
3193            self.body = str(body)
3194            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3195
3196            if "stopOrderId" in response.keys():
3197                uLogger.info(
3198                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3199                        response["stopOrderId"],
3200                        self.ticker, self.figi,
3201                        operation, lots,
3202                        targetPrice, instrument["currency"],
3203                        limitPrice, instrument["currency"],
3204                        TKS_STOP_ORDER_TYPES[stopOrderType],
3205                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3206                    ))
3207
3208                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3209                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3210                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3211                            targetPrice, instrument["currency"],
3212                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3213                        ))
3214
3215                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3216                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3217                            targetPrice, instrument["currency"],
3218                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3219                        ))
3220
3221            else:
3222                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3223
3224        return response
3225
3226    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3227        """
3228        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3229        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3230        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3231        See also: `Order()` docstring.
3232
3233        :param lots: volume, integer count of lots >= 1.
3234        :param targetPrice: target price > 0. This is open trade price for limit order.
3235        :return: JSON with response from broker server.
3236        """
3237        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3238
3239    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3240        """
3241        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3242        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3243        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3244        target price value then broker opens a limit order. See also: `Order()` docstring.
3245
3246        :param lots: volume, integer count of lots >= 1.
3247        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3248        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3249                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3250        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3251                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3252        :param expDate: string "Undefined" by default or local date in future.
3253                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3254                        This date is converting to UTC format for server.
3255        :return: JSON with response from broker server.
3256        """
3257        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3258
3259    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3260        """
3261        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3262        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3263        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3264        See also: `Order()` docstring.
3265
3266        :param lots: volume, integer count of lots >= 1.
3267        :param targetPrice: target price > 0. This is open trade price for limit order.
3268        :return: JSON with response from broker server.
3269        """
3270        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3271
3272    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3273        """
3274        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3275        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3276        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3277        target price value then broker opens a limit order. See also: `Order()` docstring.
3278
3279        :param lots: volume, integer count of lots >= 1.
3280        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3281        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3282                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3283        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3284                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3285        :param expDate: string "Undefined" by default or local date in future.
3286                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3287                        This date is converting to UTC format for server.
3288        :return: JSON with response from broker server.
3289        """
3290        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3291
3292    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3293        """
3294        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3295
3296        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3297        :param allOrdersIDs: pre-received lists of all active pending orders.
3298                             This avoids unnecessary downloading data from the server.
3299        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3300        """
3301        if self.accountId is None or not self.accountId:
3302            uLogger.error("Variable `accountId` must be defined for using this method!")
3303            raise Exception("Account ID required")
3304
3305        if orderIDs:
3306            if allOrdersIDs is None or not allOrdersIDs:
3307                rawOrders = self.RequestPendingOrders()
3308                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3309
3310            if allStopOrdersIDs is None or not allStopOrdersIDs:
3311                rawStopOrders = self.RequestStopOrders()
3312                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3313
3314            for orderID in orderIDs:
3315                idInPendingOrders = orderID in allOrdersIDs
3316                idInStopOrders = orderID in allStopOrdersIDs
3317
3318                if not (idInPendingOrders or idInStopOrders):
3319                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3320                    continue
3321
3322                else:
3323                    if idInPendingOrders:
3324                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3325
3326                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3327                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3328                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3329                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3330
3331                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3332                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3333                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3334
3335                        else:
3336                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3337
3338                    elif idInStopOrders:
3339                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3340
3341                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3342                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3343                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3344                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3345
3346                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3347                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3348                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3349
3350                        else:
3351                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3352
3353                    else:
3354                        continue
3355
3356    def CloseAllOrders(self) -> None:
3357        """
3358        Gets a list of open pending and stop orders and cancel it all.
3359        """
3360        rawOrders = self.RequestPendingOrders()
3361        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3362        lenOrders = len(allOrdersIDs)
3363
3364        rawStopOrders = self.RequestStopOrders()
3365        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3366        lenSOrders = len(allStopOrdersIDs)
3367
3368        if lenOrders > 0 or lenSOrders > 0:
3369            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3370
3371            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3372
3373        else:
3374            uLogger.info("Orders not found, nothing to cancel.")
3375
3376    def CloseAll(self, *args) -> None:
3377        """
3378        Close all available (not blocked) opened trades and orders.
3379
3380        Also, you can select one or more keywords case-insensitive:
3381        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3382
3383        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3384        """
3385        overview = self.Overview(show=False)  # get all open trades info
3386
3387        if len(args) == 0:
3388            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3389            self.CloseAllOrders()  # close all pending and stop orders
3390
3391            for iType in TKS_INSTRUMENTS:
3392                if iType != "Currencies":
3393                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3394
3395        else:
3396            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3397            lowerArgs = [x.lower() for x in args]
3398
3399            if "orders" in lowerArgs:
3400                self.CloseAllOrders()  # close all pending and stop orders
3401
3402            for iType in TKS_INSTRUMENTS:
3403                if iType.lower() in lowerArgs and iType != "Currencies":
3404                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3405
3406    @staticmethod
3407    def ParseOrderParameters(operation, **inputParameters):
3408        """
3409        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3410
3411        :param operation: string "Buy" or "Sell".
3412        :param inputParameters: this is dict of strings that looks like this
3413               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3414               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3415               "prices" key: one or more prices to open limit-orders
3416               Counts of values in lots and prices lists must be equals!
3417        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3418        """
3419        # TODO: update order grid work with api v2
3420        pass
3421        # uLogger.debug("Input parameters: {}".format(inputParameters))
3422        #
3423        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3424        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3425        #     raise Exception("Incorrect value")
3426        #
3427        # if "l" in inputParameters.keys():
3428        #     inputParameters["lots"] = inputParameters.pop("l")
3429        #
3430        # if "p" in inputParameters.keys():
3431        #     inputParameters["prices"] = inputParameters.pop("p")
3432        #
3433        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3434        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3435        #     raise Exception("Incorrect value")
3436        #
3437        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3438        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3439        #
3440        # if len(lots) != len(prices):
3441        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3442        #     raise Exception("Incorrect value")
3443        #
3444        # uLogger.debug("Extracted parameters for orders:")
3445        # uLogger.debug("lots = {}".format(lots))
3446        # uLogger.debug("prices = {}".format(prices))
3447        #
3448        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3449        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3450        # uLogger.debug("Order parameters: {}".format(result))
3451        #
3452        # return result
3453
3454    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3455        """
3456        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3457
3458        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3459        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3460        """
3461        result = False
3462        msg = "Instrument not defined!"
3463
3464        if portfolio is None or not portfolio:
3465            portfolio = self.Overview(show=False)
3466
3467        if self.ticker:
3468            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3469            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3470
3471            for iType in TKS_INSTRUMENTS:
3472                for instrument in portfolio["stat"][iType]:
3473                    if instrument["ticker"] == self.ticker:
3474                        result = True
3475                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3476                        break
3477
3478        elif self.figi:
3479            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3480            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3481
3482            for iType in TKS_INSTRUMENTS:
3483                for instrument in portfolio["stat"][iType]:
3484                    if instrument["figi"] == self.figi:
3485                        result = True
3486                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3487                        break
3488
3489        else:
3490            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3491
3492        uLogger.debug(msg)
3493
3494        return result
3495
3496    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3497        """
3498        Returns instrument is in the user's portfolio if it presents there.
3499        Instrument must be defined by `ticker` (highly priority) or `figi`.
3500
3501        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3502        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3503        """
3504        result = None
3505        msg = "Instrument not defined!"
3506
3507        if portfolio is None or not portfolio:
3508            portfolio = self.Overview(show=False)
3509
3510        if self.ticker:
3511            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3512            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3513
3514            for iType in TKS_INSTRUMENTS:
3515                for instrument in portfolio["stat"][iType]:
3516                    if instrument["ticker"] == self.ticker:
3517                        result = instrument
3518                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3519                        break
3520
3521        elif self.figi:
3522            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3523            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3524
3525            for iType in TKS_INSTRUMENTS:
3526                for instrument in portfolio["stat"][iType]:
3527                    if instrument["figi"] == self.figi:
3528                        result = instrument
3529                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3530                        break
3531
3532        else:
3533            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3534
3535        uLogger.debug(msg)
3536
3537        return result
3538
3539    def RequestLimits(self) -> dict:
3540        """
3541        Method for obtaining the available funds for withdrawal for current `accountId`.
3542
3543        See also:
3544        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3545        - `OverviewLimits()` method
3546
3547        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3548                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3549                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3550                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3551        """
3552        if self.accountId is None or not self.accountId:
3553            uLogger.error("Variable `accountId` must be defined for using this method!")
3554            raise Exception("Account ID required")
3555
3556        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3557
3558        self.body = str({"accountId": self.accountId})
3559        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3560        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3561
3562        uLogger.debug("Records about available funds for withdrawal successfully received")
3563
3564        return rawLimits
3565
3566    def OverviewLimits(self, show: bool = False) -> dict:
3567        """
3568        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3569
3570        See also: `RequestLimits()`.
3571
3572        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3573        :return: dict with raw parsed data from server and some calculated statistics about it.
3574        """
3575        if self.accountId is None or not self.accountId:
3576            uLogger.error("Variable `accountId` must be defined for using this method!")
3577            raise Exception("Account ID required")
3578
3579        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3580
3581        view = {
3582            "rawLimits": rawLimits,
3583            "limits": {  # parsed data for every currency:
3584                "money": {  # this is an array of portfolio currency positions
3585                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3586                },
3587                "blocked": {  # this is an array of blocked currency
3588                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3589                },
3590                "blockedGuarantee": {  # this is locked money under collateral for futures
3591                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3592                },
3593            },
3594        }
3595
3596        # --- Prepare text table with limits in human-readable format:
3597        if show:
3598            info = [
3599                "# Withdrawal limits\n\n",
3600                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3601                "* **Account ID:** [{}]\n".format(self.accountId),
3602                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3603                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3604            ]
3605
3606            for curr in view["limits"]["money"].keys():
3607                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3608                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3609                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3610
3611                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3612                    "[{}]".format(curr),
3613                    "{:.2f}".format(view["limits"]["money"][curr]),
3614                    "{:.2f}".format(availableMoney),
3615                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3616                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3617                )
3618
3619                if curr == "rub":
3620                    info.insert(5, infoStr)  # insert at first position in table and after headers
3621
3622                else:
3623                    info.append(infoStr)
3624
3625            infoText = "".join(info)
3626
3627            uLogger.info(infoText)
3628
3629            if self.withdrawalLimitsFile:
3630                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3631                    fH.write(infoText)
3632
3633                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3634
3635        return view
3636
3637    def RequestAccounts(self) -> dict:
3638        """
3639        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3640
3641        See also:
3642        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3643        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3644        - `OverviewUserInfo()` method
3645
3646        :return: dict with raw data from server that contains accounts info. Example of dict:
3647                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3648                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3649                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3650                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3651        """
3652        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3653
3654        self.body = str({})
3655        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3656        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3657
3658        uLogger.debug("Records about available accounts successfully received")
3659
3660        return rawAccounts
3661
3662    def RequestUserInfo(self) -> dict:
3663        """
3664        Method for requesting common user's information.
3665
3666        See also:
3667        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3668        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3669        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3670        - `OverviewUserInfo()` method
3671
3672        :return: dict with raw data from server that contains user's information. Example of dict:
3673                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3674                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3675        """
3676        uLogger.debug("Requesting common user's information. Wait, please...")
3677
3678        self.body = str({})
3679        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3680        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3681
3682        uLogger.debug("Records about current user successfully received")
3683
3684        return rawUserInfo
3685
3686    def RequestMarginStatus(self, accountId: str = None) -> dict:
3687        """
3688        Method for requesting margin calculation for defined account ID.
3689
3690        See also:
3691        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3692        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3693        - `OverviewUserInfo()` method
3694
3695        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3696        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3697                 Example of responses:
3698                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3699                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3700                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3701                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3702                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3703                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3704        """
3705        if accountId is None or not accountId:
3706            if self.accountId is None or not self.accountId:
3707                uLogger.error("Variable `accountId` must be defined for using this method!")
3708                raise Exception("Account ID required")
3709
3710            else:
3711                accountId = self.accountId  # use `self.accountId` (main ID) by default
3712
3713        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3714
3715        self.body = str({"accountId": accountId})
3716        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3717        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3718
3719        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3720            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3721            rawMargin = {}
3722
3723        else:
3724            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3725
3726        return rawMargin
3727
3728    def RequestTariffLimits(self) -> dict:
3729        """
3730        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3731
3732        See also:
3733        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3734        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3735        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3736        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3737        - `OverviewUserInfo()` method
3738
3739        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3740                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3741                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3742        """
3743        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3744
3745        self.body = str({})
3746        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3747        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3748
3749        uLogger.debug("Records with limits of current tariff successfully received")
3750
3751        return rawTariffLimits
3752
3753    def RequestBondCoupons(self, iJSON: dict) -> dict:
3754        """
3755        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3756        then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
3757        All dates are in UTC timezone.
3758
3759        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3760        Documentation:
3761        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3762        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3763
3764        See also: `ExtendBondsData()`.
3765
3766        :param iJSON: raw json data of a bond from broker server, example: `iJSON = self.iList["Bonds"][self.ticker]`
3767                      If raw iJSON is not data of bond then server returns an error [400] with message:
3768                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3769        :return: dictionary with bond payment calendar. Response example:
3770                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3771                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3772                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3773                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3774        """
3775        if iJSON["figi"] is None or not iJSON["figi"]:
3776            uLogger.error("FIGI must be defined for using this method!")
3777            raise Exception("FIGI required")
3778
3779        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3780        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3781
3782        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3783            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3784            self.figi,
3785            startDate,
3786            endDate,
3787        ))
3788
3789        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3790        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3791        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3792
3793        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3794            uLogger.warning("Instrument type is not bond!")
3795
3796        else:
3797            uLogger.debug("Records about bond payment calendar successfully received")
3798
3799        return calendar
3800
3801    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3802        """
3803        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3804        pandas dataframe with more information about bonds: main info, current prices, bond payment calendar,
3805        coupon yields, current yields and some statistics etc.
3806
3807        WARNING! This is too long operation if a lot of bonds requested from broker server.
3808
3809        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3810
3811        :param instruments: list of strings with tickers or FIGIs.
3812        :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default: `ext-bonds.xlsx`,
3813                     for further used by data scientists or stock analytics.
3814        :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker.
3815                 In XLSX-file and pandas dataframe fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3816        """
3817        if instruments is None or not instruments:
3818            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3819            raise Exception("Ticker or FIGI required")
3820
3821        if isinstance(instruments, str):
3822            instruments = [instruments]
3823
3824        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3825
3826        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3827
3828        iCount = len(uniqueInstruments)
3829        tooLong = iCount >= 20
3830        if tooLong:
3831            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3832
3833        bonds = None
3834        for i, self.figi in enumerate(uniqueInstruments):
3835            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3836
3837            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3838                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3839                rawBond = self.SearchByFIGI(requestPrice=True)
3840
3841                # Widen raw data with UTC current time (iData["actualDateTime"]):
3842                actualDate = datetime.now(tzutc())
3843                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3844
3845                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3846                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3847
3848                # Replace some values with human-readable:
3849                iData["nominalCurrency"] = iData["nominal"]["currency"]
3850                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3851                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3852                iData["aciCurrency"] = iData["aciValue"]["currency"]
3853                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3854                iData["issueSize"] = int(iData["issueSize"])
3855                iData["issueSizePlan"] = int(iData["issueSize"])
3856                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3857                iData["minPriceIncrement"] = NanoToFloat(iData["minPriceIncrement"]["units"], iData["minPriceIncrement"]["nano"]) if "minPriceIncrement" in iData.keys() else 0.
3858                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3859
3860                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3861                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3862                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3863                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3864                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3865                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3866                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3867                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3868                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3869                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3870                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3871
3872                # Widen raw data with calendar data from `rawCalendar` values:
3873                calendarData = []
3874                for item in iData["rawCalendar"]["events"]:
3875                    calendarData.append({
3876                        "couponDate": item["couponDate"],
3877                        "couponNumber": int(item["couponNumber"]),
3878                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3879                        "payCurrency": item["payOneBond"]["currency"],
3880                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3881                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3882                        "couponStartDate": item["couponStartDate"],
3883                        "couponEndDate": item["couponEndDate"],
3884                        "couponPeriod": item["couponPeriod"],
3885                    })
3886
3887                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3888                if "maturityDate" not in iData.keys():
3889                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3890
3891                # Widen raw data with Coupon Rate.
3892                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3893                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3894                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3895                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3896
3897                # Widen raw data with Yield to Maturity (YTM) on current date.
3898                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3899                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3900                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3901                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3902                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3903                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3904
3905                iData["calendar"] = calendarData  # adds calendar at the end
3906
3907                # Remove not used data:
3908                iData.pop("uid")
3909                iData.pop("positionUid")
3910                iData.pop("currentPrice")
3911                iData.pop("rawCalendar")
3912
3913                colNames = list(iData.keys())
3914                if bonds is None:
3915                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3916
3917                else:
3918                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3919
3920            else:
3921                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3922
3923            processed = round(100 * (i + 1) / iCount, 1)
3924            if tooLong and processed % 5 == 0:
3925                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3926
3927            else:
3928                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3929
3930        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3931
3932        # Saving bonds from pandas dataframe to XLSX sheet:
3933        if xlsx and self.bondsXLSXFile:
3934            with pd.ExcelWriter(
3935                    path=self.bondsXLSXFile,
3936                    date_format=TKS_DATE_FORMAT,
3937                    datetime_format=TKS_DATE_TIME_FORMAT,
3938                    mode="w",
3939            ) as writer:
3940                bonds.to_excel(
3941                    writer,
3942                    sheet_name="Extended bonds data",
3943                    index=True,
3944                    encoding="UTF-8",
3945                    freeze_panes=(1, 1),
3946                )  # saving as XLSX-file with freeze first row and column as headers
3947
3948            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3949
3950        return bonds
3951
3952    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3953        """
3954        Creates bond payments calendar as pandas dataframe, and you can also save it to the XLSX-file.
3955
3956        WARNING! This is too long operation if a lot of bonds requested from broker server.
3957
3958        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3959
3960        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
3961                        extended information about bonds: main info, current prices, bond payment calendar,
3962                        coupon yields, current yields and some statistics etc.
3963                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3964        :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, default: `calendar.xlsx`,
3965                     for further used by data scientists or stock analytics.
3966        :return: pandas dataframe with only bond payments calendar data.
3967        """
3968        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3969            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3970
3971        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3972
3973        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3974        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3975        calendar = None
3976        for bond in extBonds.iterrows():
3977            for item in bond[1]["calendar"]:
3978                cData = {
3979                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3980                    "couponDate": item["couponDate"],
3981                    "figi": bond[1]["figi"],
3982                    "ticker": bond[1]["ticker"],
3983                    "name": bond[1]["name"],
3984                    "couponNumber": item["couponNumber"],
3985                    "payOneBond": item["payOneBond"],
3986                    "payCurrency": item["payCurrency"],
3987                    "couponType": item["couponType"],
3988                    "couponPeriod": item["couponPeriod"],
3989                    "fixDate": item["fixDate"],
3990                    "couponStartDate": item["couponStartDate"],
3991                    "couponEndDate": item["couponEndDate"],
3992                }
3993
3994                if calendar is None:
3995                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
3996
3997                else:
3998                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
3999
4000        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4001
4002        # Saving calendar from pandas dataframe to XLSX sheet:
4003        if xlsx:
4004            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4005
4006            with pd.ExcelWriter(
4007                    path=xlsxCalendarFile,
4008                    date_format=TKS_DATE_FORMAT,
4009                    datetime_format=TKS_DATE_TIME_FORMAT,
4010                    mode="w",
4011            ) as writer:
4012                humanReadable = calendar.copy(deep=True)
4013                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4014                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4015                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4016                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4017                humanReadable.columns = colNames  # human-readable column names
4018
4019                humanReadable.to_excel(
4020                    writer,
4021                    sheet_name="Bond payments calendar",
4022                    index=False,
4023                    encoding="UTF-8",
4024                    freeze_panes=(1, 2),
4025                )  # saving as XLSX-file with freeze first row and column as headers
4026
4027                del humanReadable  # release df in memory
4028
4029            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4030
4031        return calendar
4032
4033    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4034        """
4035        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4036
4037        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
4038
4039        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
4040                        extended information about bonds: main info, current prices, bond payment calendar,
4041                        coupon yields, current yields and some statistics etc.
4042                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4043        :param show: if `True` then also printing bonds payment calendar to the console,
4044                     otherwise save to file `calendarFile` only. `False` by default.
4045        :return: multilines text in Markdown format with bonds payment calendar as a table.
4046        """
4047        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4048            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4049
4050        infoText = "# Bond payments calendar\n\n"
4051
4052        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate pandas dataframe with full calendar data
4053
4054        if not calendar.empty:
4055            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4056
4057            info = [
4058                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4059                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4060            ]
4061
4062            newMonth = False
4063            notOneBond = calendar["figi"].nunique() > 1
4064            for i, bond in enumerate(calendar.iterrows()):
4065                if newMonth and notOneBond:
4066                    info.append(splitLine)
4067
4068                info.append(
4069                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4070                        "  +" if bond[1]["paid"] else "  —",
4071                        bond[1]["couponDate"].split("T")[0],
4072                        bond[1]["figi"],
4073                        bond[1]["ticker"],
4074                        bond[1]["couponNumber"],
4075                        "{} {}".format(
4076                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4077                            bond[1]["payCurrency"],
4078                        ),
4079                        bond[1]["couponType"],
4080                        bond[1]["couponPeriod"],
4081                        bond[1]["fixDate"].split("T")[0],
4082                    )
4083                )
4084
4085                if i < len(calendar.values) - 1:
4086                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4087                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4088                    newMonth = False if curDate.month == nextDate.month else True
4089
4090                else:
4091                    newMonth = False
4092
4093            infoText += "".join(info)
4094
4095            if show:
4096                uLogger.info("{}".format(infoText))
4097
4098            if self.calendarFile is not None:
4099                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4100                    fH.write(infoText)
4101
4102                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4103
4104        else:
4105            infoText += "No data\n"
4106
4107        return infoText
4108
4109    def OverviewAccounts(self, show: bool = False) -> dict:
4110        """
4111        Method for parsing and show simple table with all available user accounts.
4112
4113        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4114
4115        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4116        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4117                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4118                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4119                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4120                                                        "closed": "—", "access": "Full access" }, ...}}`
4121        """
4122        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4123
4124        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4125        accounts = {
4126            item["id"]: {
4127                "type": TKS_ACCOUNT_TYPES[item["type"]],
4128                "name": item["name"],
4129                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4130                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4131                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4132                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4133            } for item in rawAccounts["accounts"]
4134        }
4135
4136        # Raw and parsed data with some fields replaced in "stat" section:
4137        view = {
4138            "rawAccounts": rawAccounts,
4139            "stat": accounts,
4140        }
4141
4142        # --- Prepare simple text table with only accounts data in human-readable format:
4143        if show:
4144            info = [
4145                "# User accounts\n\n",
4146                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4147                "| Account ID   | Type                      | Status                    | Name                           |\n",
4148                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4149            ]
4150
4151            for account in view["stat"].keys():
4152                info.extend([
4153                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4154                        account,
4155                        view["stat"][account]["type"],
4156                        view["stat"][account]["status"],
4157                        view["stat"][account]["name"],
4158                    )
4159                ])
4160
4161            infoText = "".join(info)
4162
4163            uLogger.info(infoText)
4164
4165            if self.userAccountsFile:
4166                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4167                    fH.write(infoText)
4168
4169                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4170
4171        return view
4172
4173    def OverviewUserInfo(self, show: bool = False) -> dict:
4174        """
4175        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4176
4177        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4178
4179        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4180        :return: dict with raw parsed data from server and some calculated statistics about it.
4181        """
4182        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4183        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4184        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4185        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4186        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4187        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4188
4189        # This is dict with parsed common user data:
4190        userInfo = {
4191            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4192            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4193            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4194            "tariff": rawUserInfo["tariff"],
4195        }
4196
4197        # This is an array of dict with parsed margin statuses for every account IDs:
4198        margins = {}
4199        for accountId in accounts.keys():
4200            if rawMargins[accountId]:
4201                margins[accountId] = {
4202                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4203                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4204                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4205                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4206                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4207                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4208                }
4209
4210            else:
4211                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4212
4213        unary = {}  # unary-connection limits
4214        for item in rawTariffLimits["unaryLimits"]:
4215            if item["limitPerMinute"] in unary.keys():
4216                unary[item["limitPerMinute"]].extend(item["methods"])
4217
4218            else:
4219                unary[item["limitPerMinute"]] = item["methods"]
4220
4221        stream = {}  # stream-connection limits
4222        for item in rawTariffLimits["streamLimits"]:
4223            if item["limit"] in stream.keys():
4224                stream[item["limit"]].extend(item["streams"])
4225
4226            else:
4227                stream[item["limit"]] = item["streams"]
4228
4229        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4230        limits = {
4231            "unary": unary,
4232            "stream": stream,
4233        }
4234
4235        # Raw and parsed data as an output result:
4236        view = {
4237            "rawUserInfo": rawUserInfo,
4238            "rawAccounts": rawAccounts,
4239            "rawMargins": rawMargins,
4240            "rawTariffLimits": rawTariffLimits,
4241            "stat": {
4242                "userInfo": userInfo,
4243                "accounts": accounts,
4244                "margins": margins,
4245                "limits": limits,
4246            },
4247        }
4248
4249        # --- Prepare text table with user information in human-readable format:
4250        if show:
4251            info = [
4252                "# Full user information\n\n",
4253                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4254                "## Common information\n\n",
4255                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4256                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4257                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4258                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4259                "\n## User accounts\n\n",
4260            ]
4261
4262            for account in view["stat"]["accounts"].keys():
4263                info.extend([
4264                    "### ID: [{}]\n\n".format(account),
4265                    "| Parameters           | Values                                                       |\n",
4266                    "|----------------------|--------------------------------------------------------------|\n",
4267                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4268                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4269                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4270                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4271                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4272                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4273                ])
4274
4275                if margins[account]:
4276                    info.extend([
4277                        "| Margin status:       | Enabled                                                      |\n",
4278                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4279                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4280                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4281                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4282                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4283                    ])
4284
4285                else:
4286                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4287
4288            info.extend([
4289                "\n## Current user tariff limits\n",
4290                "\nSee also:\n",
4291                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4292                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4293                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4294                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4295                "\n### Unary limits\n",
4296            ])
4297
4298            if unary:
4299                for key, values in sorted(unary.items()):
4300                    info.append("\n* Max requests per minute: {}\n".format(key))
4301
4302                    for value in values:
4303                        info.append("  - {}\n".format(value))
4304
4305            else:
4306                info.append("\nNot available\n")
4307
4308            info.append("\n### Stream limits\n")
4309
4310            if stream:
4311                for key, values in sorted(stream.items()):
4312                    info.append("\n* Max stream connections: {}\n".format(key))
4313
4314                    for value in values:
4315                        info.append("  - {}\n".format(value))
4316
4317            else:
4318                info.append("\nNot available\n")
4319
4320            infoText = "".join(info)
4321
4322            uLogger.info(infoText)
4323
4324            if self.userInfoFile:
4325                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4326                    fH.write(infoText)
4327
4328                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4329
4330        return view
4331
4332
4333class Args:
4334    """
4335    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4336    """
4337    def __init__(self, **kwargs):
4338        self.__dict__.update(kwargs)
4339
4340    def __getattr__(self, item):
4341        return None
4342
4343
4344def ParseArgs():
4345    """
4346    Function get and parse command line keys. See examples: https://tim55667757.github.io/TKSBrokerAPI/
4347    """
4348    parser = ArgumentParser()  # command-line string parser
4349
4350    parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples"
4351    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4352
4353    # --- options:
4354
4355    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.")
4356    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4357    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4358
4359    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4360    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4361
4362    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4363    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4364
4365    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4366
4367    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4368    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4369    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4370
4371    parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4372
4373    # --- commands:
4374
4375    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4376
4377    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4378    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4379    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to xlsx-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4380    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4381    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4382    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present, and also saved to file `calendar.xlsx` by default. Also, if the `--output` key present then calendar saves to file, default: `calendar.md`. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4383    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4384    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4385
4386    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4387    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4388    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4389    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4390    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4391
4392    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4393    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4394    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4395    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4396
4397    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4398    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4399    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4400
4401    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4402    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4403    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4404    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4405    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4406    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4407    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4408
4409    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4410    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4411    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.")
4412    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.")
4413    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4414
4415    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4416    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4417    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4418
4419    cmdArgs = parser.parse_args()
4420    return cmdArgs
4421
4422
4423def Main(**kwargs):
4424    """
4425    Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.
4426
4427    See examples: https://tim55667757.github.io/TKSBrokerAPI/
4428    """
4429    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4430
4431    if args.debug_level:
4432        uLogger.level = 10  # always debug level by default
4433        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4434
4435    exitCode = 0
4436    start = datetime.now(tzutc())
4437    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4438        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4439        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4440    ))
4441
4442    # trying to calculate full current version:
4443    buildVersion = __version__
4444    try:
4445        v = version("tksbrokerapi")
4446        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4447
4448    except Exception:
4449        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4450
4451    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4452    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4453
4454    try:
4455        if args.version:
4456            print("TKSBrokerAPI {}".format(buildVersion))
4457            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4458
4459        else:
4460            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4461            server = TinkoffBrokerServer(
4462                token=args.token,
4463                accountId=args.account_id,
4464                useCache=not args.no_cache,
4465            )
4466
4467            # --- set some options:
4468
4469            if args.ticker:
4470                if args.ticker in server.aliasesKeys:
4471                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4472
4473                else:
4474                    server.ticker = args.ticker
4475
4476            if args.figi:
4477                server.figi = args.figi
4478
4479            if args.depth is not None:
4480                server.depth = args.depth
4481
4482            # --- do one of commands:
4483
4484            if args.list:
4485                if args.output is not None:
4486                    server.instrumentsFile = args.output
4487
4488                server.ShowInstrumentsInfo(show=True)
4489
4490            elif args.list_xlsx:
4491                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4492
4493            elif args.bonds_xlsx is not None:
4494                if args.output is not None:
4495                    server.bondsXLSXFile = args.output
4496
4497                if len(args.bonds_xlsx) == 0:
4498                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4499
4500                else:
4501                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4502
4503            elif args.search:
4504                if args.output is not None:
4505                    server.searchResultsFile = args.output
4506
4507                server.SearchInstruments(pattern=args.search[0], show=True)
4508
4509            elif args.info:
4510                if not (args.ticker or args.figi):
4511                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4512                    raise Exception("Ticker or FIGI required")
4513
4514                if args.output is not None:
4515                    server.infoFile = args.output
4516
4517                if args.ticker:
4518                    server.SearchByTicker(requestPrice=True, show=True, debug=False)  # show info and current prices by ticker name
4519
4520                else:
4521                    server.SearchByFIGI(requestPrice=True, show=True, debug=False)  # show info and current prices by FIGI id
4522
4523            elif args.calendar is not None:
4524                if args.output is not None:
4525                    server.calendarFile = args.output
4526
4527                if len(args.calendar) == 0:
4528                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4529
4530                else:
4531                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4532
4533                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4534
4535            elif args.price:
4536                if not (args.ticker or args.figi):
4537                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4538                    raise Exception("Ticker or FIGI required")
4539
4540                server.GetCurrentPrices(show=True)
4541
4542            elif args.prices is not None:
4543                if args.output is not None:
4544                    server.pricesFile = args.output
4545
4546                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4547
4548            elif args.overview:
4549                if args.output is not None:
4550                    server.overviewFile = args.output
4551
4552                server.Overview(show=True, details="full")
4553
4554            elif args.overview_digest:
4555                if args.output is not None:
4556                    server.overviewDigestFile = args.output
4557
4558                server.Overview(show=True, details="digest")
4559
4560            elif args.overview_positions:
4561                if args.output is not None:
4562                    server.overviewPositionsFile = args.output
4563
4564                server.Overview(show=True, details="positions")
4565
4566            elif args.overview_orders:
4567                if args.output is not None:
4568                    server.overviewOrdersFile = args.output
4569
4570                server.Overview(show=True, details="orders")
4571
4572            elif args.overview_analytics:
4573                if args.output is not None:
4574                    server.overviewAnalyticsFile = args.output
4575
4576                server.Overview(show=True, details="analytics")
4577
4578            elif args.deals is not None:
4579                if args.output is not None:
4580                    server.reportFile = args.output
4581
4582                if 0 <= len(args.deals) < 3:
4583                    server.Deals(
4584                        start=args.deals[0] if len(args.deals) >= 1 else None,
4585                        end=args.deals[1] if len(args.deals) == 2 else None,
4586                        show=True,  # Always show deals report in console
4587                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4588                    )
4589
4590                else:
4591                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4592                    raise Exception("Incorrect value")
4593
4594            elif args.history is not None:
4595                if args.output is not None:
4596                    server.historyFile = args.output
4597
4598                if 0 <= len(args.history) < 3:
4599                    dataReceived = server.History(
4600                        start=args.history[0] if len(args.history) >= 1 else None,
4601                        end=args.history[1] if len(args.history) == 2 else None,
4602                        interval="hour" if args.interval is None or not args.interval else args.interval,
4603                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4604                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4605                        show=True,  # shows all downloaded candles in console
4606                    )
4607
4608                    if args.render_chart is not None and dataReceived is not None:
4609                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4610
4611                        server.ShowHistoryChart(
4612                            candles=dataReceived,
4613                            interact=iChart,
4614                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4615                        )
4616
4617                else:
4618                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4619                    raise Exception("Incorrect value")
4620
4621            elif args.load_history is not None:
4622                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4623
4624                if args.render_chart is not None and histData is not None:
4625                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4626                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4627
4628                    server.ShowHistoryChart(
4629                        candles=histData,
4630                        interact=iChart,
4631                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4632                    )
4633
4634            elif args.trade is not None:
4635                if 1 <= len(args.trade) <= 5:
4636                    server.Trade(
4637                        operation=args.trade[0],
4638                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4639                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4640                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4641                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4642                    )
4643
4644                else:
4645                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4646
4647            elif args.buy is not None:
4648                if 0 <= len(args.buy) <= 4:
4649                    server.Buy(
4650                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4651                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4652                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4653                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4654                    )
4655
4656                else:
4657                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4658
4659            elif args.sell is not None:
4660                if 0 <= len(args.sell) <= 4:
4661                    server.Sell(
4662                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4663                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4664                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4665                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4666                    )
4667
4668                else:
4669                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4670
4671            elif args.order:
4672                if 4 <= len(args.order) <= 7:
4673                    server.Order(
4674                        operation=args.order[0],
4675                        orderType=args.order[1],
4676                        lots=int(args.order[2]),
4677                        targetPrice=float(args.order[3]),
4678                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4679                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4680                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4681                    )
4682
4683                else:
4684                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4685
4686            elif args.buy_limit:
4687                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4688
4689            elif args.sell_limit:
4690                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4691
4692            elif args.buy_stop:
4693                if 2 <= len(args.buy_stop) <= 7:
4694                    server.BuyStop(
4695                        lots=int(args.buy_stop[0]),
4696                        targetPrice=float(args.buy_stop[1]),
4697                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4698                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4699                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4700                    )
4701
4702                else:
4703                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4704
4705            elif args.sell_stop:
4706                if 2 <= len(args.sell_stop) <= 7:
4707                    server.SellStop(
4708                        lots=int(args.sell_stop[0]),
4709                        targetPrice=float(args.sell_stop[1]),
4710                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4711                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4712                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4713                    )
4714
4715                else:
4716                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4717
4718            # elif args.buy_order_grid is not None:
4719            #     # update order grid work with api v2
4720            #     if len(args.buy_order_grid) == 2:
4721            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4722            #
4723            #         for order in orderParams:
4724            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4725            #
4726            #     else:
4727            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4728            #
4729            # elif args.sell_order_grid is not None:
4730            #     # update order grid work with api v2
4731            #     if len(args.sell_order_grid) >= 2:
4732            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4733            #
4734            #         for order in orderParams:
4735            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4736            #
4737            #     else:
4738            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4739
4740            elif args.close_order is not None:
4741                server.CloseOrders(args.close_order)  # close only one order
4742
4743            elif args.close_orders is not None:
4744                server.CloseOrders(args.close_orders)  # close list of orders
4745
4746            elif args.close_trade:
4747                if not args.ticker:
4748                    uLogger.error("`--ticker` key is required for this operation!")
4749                    raise Exception("Ticker required")
4750
4751                server.CloseTrades([args.ticker])  # close only one trade
4752
4753            elif args.close_trades is not None:
4754                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4755
4756            elif args.close_all is not None:
4757                server.CloseAll(*args.close_all)
4758
4759            elif args.limits:
4760                if args.output is not None:
4761                    server.withdrawalLimitsFile = args.output
4762
4763                server.OverviewLimits(show=True)
4764
4765            elif args.user_info:
4766                if args.output is not None:
4767                    server.userInfoFile = args.output
4768
4769                server.OverviewUserInfo(show=True)
4770
4771            elif args.account:
4772                if args.output is not None:
4773                    server.userAccountsFile = args.output
4774
4775                server.OverviewAccounts(show=True)
4776
4777            else:
4778                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4779                raise Exception("There is no command to execute")
4780
4781    except Exception:
4782        trace = tb.format_exc()
4783        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4784            if e in trace:
4785                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4786                break
4787
4788        uLogger.debug(trace)
4789        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4790        exitCode = 255  # an error occurred, must be open a ticket for this issue
4791
4792    finally:
4793        finish = datetime.now(tzutc())
4794
4795        if exitCode == 0:
4796            uLogger.debug("All operations were finished success (summary code is 0).")
4797
4798        else:
4799            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4800                os.path.abspath(uLog.defaultLogFile), exitCode,
4801            ))
4802
4803        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4804        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4805            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4806            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4807        ))
4808
4809        if not kwargs:
4810            sys.exit(exitCode)
4811
4812        else:
4813            return exitCode
4814
4815
4816if __name__ == "__main__":
4817    Main()
def NanoToFloat(units: str, nano: int) -> float:
78def NanoToFloat(units: str, nano: int) -> float:
79    """
80    Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples:
81
82    `NanoToFloat(units="2", nano=500000000) -> 2.5`
83
84    `NanoToFloat(units="0", nano=50000000) -> 0.05`
85
86    :param units: integer string or integer parameter that represents the integer part of number
87    :param nano: integer string or integer parameter that represents the fractional part of number
88    :return: float view of number
89    """
90    return int(units) + int(nano) * NANO

Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:

NanoToFloat(units="2", nano=500000000) -> 2.5

NanoToFloat(units="0", nano=50000000) -> 0.05

Parameters
  • units: integer string or integer parameter that represents the integer part of number
  • nano: integer string or integer parameter that represents the fractional part of number
Returns

float view of number

def FloatToNano(number: float) -> dict:
 93def FloatToNano(number: float) -> dict:
 94    """
 95    Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples:
 96
 97    `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}`
 98
 99    `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}`
100
101    :param number: float number
102    :return: nano-type view of number: `{"units": "string", "nano": integer}`
103    """
104    splitByPoint = str(number).split(".")
105    frac = 0
106
107    if len(splitByPoint) > 1:
108        if len(splitByPoint[1]) <= 9:
109            frac = int("{}{}".format(
110                int(splitByPoint[1]),
111                "0" * (9 - len(splitByPoint[1])),
112            ))
113
114    if (number < 0) and (frac > 0):
115        frac = -frac
116
117    return {"units": str(int(number)), "nano": frac}

Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:

FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}

FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}

Parameters
  • number: float number
Returns

nano-type view of number: {"units": "string", "nano": integer}

def GetDatesAsString(start: str = None, end: str = None) -> tuple:
120def GetDatesAsString(start: str = None, end: str = None) -> tuple:
121    """
122    Create tuple of date and time strings with timezone parsed from user-friendly date.
123
124    User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020).
125
126    Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")
127    An error exception will occur if input date has incorrect format.
128
129    If `start=None`, `end=None` then return dates from yesterday to the end of the day.
130    If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day.
131    If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`.
132    Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago.
133
134    Also, you can use keywords for start if `end=None`:
135    `today` (from 00:00:00 to the end of current day),
136    `yesterday` (-1 day from 00:00:00 to 23:59:59),
137    `week` (-7 day from 00:00:00 to the end of current day),
138    `month` (-30 day from 00:00:00 to the end of current day),
139    `year` (-365 day from 00:00:00 to the end of current day),
140
141    :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI.
142             See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`.
143             Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day.
144    """
145    uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end))
146    s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0)  # start of the current day
147    e = s.replace(hour=23, minute=59, second=59, microsecond=0)  # end of the current day
148
149    # time between start and the end of the current day:
150    if start is None or start.lower() == "today":
151        pass
152
153    # from start of the last day to the end of the last day:
154    elif start.lower() == "yesterday":
155        s -= timedelta(days=1)
156        e -= timedelta(days=1)
157
158    # week (-7 day from 00:00:00 to the end of the current day):
159    elif start.lower() == "week":
160        s -= timedelta(days=6)  # +1 current day already taken into account
161
162    # month (-30 day from 00:00:00 to the end of current day):
163    elif start.lower() == "month":
164        s -= timedelta(days=29)  # +1 current day already taken into account
165
166    # year (-365 day from 00:00:00 to the end of current day):
167    elif start.lower() == "year":
168        s -= timedelta(days=364)  # +1 current day already taken into account
169
170    # -N days ago to the end of current day:
171    elif start.startswith('-') and start[1:].isdigit():
172        s -= timedelta(days=abs(int(start)) - 1)  # +1 current day already taken into account
173
174    # dates between start day at 00:00:00 and the end of the last day at 23:59:59:
175    else:
176        s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc())
177        e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e
178
179    # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API:
180    s = s.strftime(TKS_DATE_TIME_FORMAT)
181    e = e.strftime(TKS_DATE_TIME_FORMAT)
182
183    uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e))
184
185    return s, e

Create tuple of date and time strings with timezone parsed from user-friendly date.

User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).

Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.

If start=None, end=None then return dates from yesterday to the end of the day. If start=some_date_1, end=None then return dates from some_date_1 to the end of the day. If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2. Start day may be negative integer numbers: -1, -2, -3 - how many days ago.

Also, you can use keywords for start if end=None: today (from 00:00:00 to the end of current day), yesterday (-1 day from 00:00:00 to 23:59:59), week (-7 day from 00:00:00 to the end of current day), month (-30 day from 00:00:00 to the end of current day), year (-365 day from 00:00:00 to the end of current day),

Returns

tuple with 2 strings (start, end) dates in UTC ISO time format %Y-%m-%dT%H:%M:%SZ for OpenAPI. See date and time format here: TKSEnums.TKS_DATE_TIME_FORMAT. Example: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.

class TinkoffBrokerServer:
 188class TinkoffBrokerServer:
 189    """
 190    This class implements methods to work with Tinkoff broker server.
 191
 192    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
 193
 194    About `token`: https://tinkoff.github.io/investAPI/token/
 195    """
 196    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 197        """
 198        Main class init.
 199
 200        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 201        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 202                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 203        :param useCache: use default cache file with raw data to use instead of `iList`.
 204                         True by default. Cache is auto-update if new day has come.
 205                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 206        :param defaultCache: path to default cache file. `dump.json` by default.
 207        """
 208        if token is None or not token:
 209            try:
 210                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 211                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 212
 213            except KeyError:
 214                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 215                raise Exception("Token required")
 216
 217        else:
 218            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 219            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 220
 221        if accountId is None or not accountId:
 222            try:
 223                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 224                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 225
 226            except KeyError:
 227                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 228
 229        else:
 230            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 231            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 232
 233        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 234        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 235
 236        Latest version: https://pypi.org/project/tksbrokerapi/
 237        """
 238
 239        self.aliases = TKS_TICKER_ALIASES
 240        """Some aliases instead official tickers.
 241
 242        See also: `TKSEnums.TKS_TICKER_ALIASES`
 243        """
 244
 245        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 246
 247        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 248
 249        self.ticker = ""
 250        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 251
 252        See also: `SearchByTicker()`, `SearchInstruments()`.
 253        """
 254
 255        self.figi = ""
 256        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
 257
 258        See also: `SearchByFIGI()`, `SearchInstruments()`.
 259        """
 260
 261        self.depth = 1
 262        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 263
 264        See also: `GetCurrentPrices()`.
 265        """
 266
 267        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 268        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 269
 270        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 271        """
 272
 273        uLogger.debug("Broker API server: {}".format(self.server))
 274
 275        self.timeout = 15
 276        """Server operations timeout in seconds. Default: `15`.
 277
 278        See also: `SendAPIRequest()`.
 279        """
 280
 281        self.headers = {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {}".format(self.token)}
 282        """Headers which send in every request to broker server. Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 283
 284        See also: `SendAPIRequest()`.
 285        """
 286
 287        self.body = None
 288        """Request body which send to broker server. Default: `None`.
 289
 290        See also: `SendAPIRequest()`.
 291        """
 292
 293        self.historyFile = None
 294        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe.
 295
 296        See also: `History()`.
 297        """
 298
 299        self.htmlHistoryFile = "index.html"
 300        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 301
 302        See also: `ShowHistoryChart()`.
 303        """
 304
 305        self.instrumentsFile = "instruments.md"
 306        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 307
 308        See also: `ShowInstrumentsInfo()`.
 309        """
 310
 311        self.searchResultsFile = "search-results.md"
 312        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 313
 314        See also: `SearchInstruments()`.
 315        """
 316
 317        self.pricesFile = "prices.md"
 318        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 319
 320        See also: `GetListOfPrices()`.
 321        """
 322
 323        self.infoFile = "info.md"
 324        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 325
 326        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 327        """
 328
 329        self.bondsXLSXFile = "ext-bonds.xlsx"
 330        """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 
 331        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 332
 333        See also: `ExtendBondsData()`.
 334        """
 335
 336        self.calendarFile = "calendar.md"
 337        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 338        
 339        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 340
 341        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 342        """
 343
 344        self.overviewFile = "overview.md"
 345        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 346
 347        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 348        """
 349
 350        self.overviewDigestFile = "overview-digest.md"
 351        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 352
 353        See also: `Overview()` with parameter `details="digest"`.
 354        """
 355
 356        self.overviewPositionsFile = "overview-positions.md"
 357        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 358
 359        See also: `Overview()` with parameter `details="positions"`.
 360        """
 361
 362        self.overviewOrdersFile = "overview-orders.md"
 363        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 364
 365        See also: `Overview()` with parameter `details="orders"`.
 366        """
 367
 368        self.overviewAnalyticsFile = "overview-analytics.md"
 369        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 370
 371        See also: `Overview()` with parameter `details="analytics"`.
 372        """
 373
 374        self.reportFile = "deals.md"
 375        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 376
 377        See also: `Deals()`.
 378        """
 379
 380        self.withdrawalLimitsFile = "limits.md"
 381        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 382
 383        See also: `OverviewLimits()` and `RequestLimits()`.
 384        """
 385
 386        self.userInfoFile = "user-info.md"
 387        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 388
 389        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 390        """
 391
 392        self.userAccountsFile = "accounts.md"
 393        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 394
 395        See also: `OverviewAccounts()`, `RequestAccounts()`.
 396        """
 397
 398        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 399        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 400
 401        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 402
 403        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 404        """
 405
 406        self.iList = None  # init iList for raw instruments data
 407        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 408        
 409        See also: `Listing()`, `DumpInstruments()`.
 410        """
 411
 412        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 413        if useCache:
 414            if os.path.exists(self.iListDumpFile):
 415                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 416                curTime = datetime.now(tzutc())
 417
 418                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 419                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 420
 421                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 422
 423                else:
 424                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 425
 426                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
 427                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 428
 429            else:
 430                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 431                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 432
 433        else:
 434            self.iList = self.Listing()  # request new raw instruments data from broker server
 435            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 436
 437        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 438        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 439
 440        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 441        """
 442
 443    @staticmethod
 444    def _ParseJSON(rawData="{}", debug: bool = False) -> dict:
 445        """
 446        Parse JSON from response string.
 447
 448        :param rawData: this is a string with JSON-formatted text.
 449        :param debug: if `True` then print more debug information.
 450        :return: JSON (dictionary), parsed from server response string.
 451        """
 452        if debug:
 453            uLogger.debug("Raw text body:")
 454            uLogger.debug(rawData)
 455
 456        responseJSON = json.loads(rawData) if rawData else {}
 457
 458        if debug:
 459            uLogger.debug("JSON formatted:")
 460            for jsonLine in json.dumps(responseJSON, indent=4).split('\n'):
 461                uLogger.debug(jsonLine)
 462
 463        return responseJSON
 464
 465    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
 466        """
 467        Send GET or POST request to broker server and receive JSON object.
 468
 469        self.header: must be defining with dictionary of headers.
 470        self.body: if define then used as request body. None by default.
 471        self.timeout: global request timeout, 15 seconds by default.
 472        :param url: url with REST request.
 473        :param reqType: send "GET" or "POST" request. "GET" by default.
 474        :param retry: how many times retry after first request if an 5xx server errors occurred.
 475        :param pause: sleep time in seconds between retries.
 476        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
 477        :return: response JSON (dictionary) from broker.
 478        """
 479        if reqType not in ("GET", "POST"):
 480            uLogger.error("You can define request type: 'GET' or 'POST'!")
 481            raise Exception("Incorrect value")
 482
 483        if debug:
 484            uLogger.debug("Request parameters:")
 485            uLogger.debug("    - REST API URL: {}".format(url))
 486            uLogger.debug("    - request type: {}".format(reqType))
 487            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
 488            uLogger.debug("    - body: {}".format(self.body))
 489
 490        # fast hack to avoid all operations with some tickers/FIGI
 491        responseJSON = {}
 492        oK = True
 493        for item in self.exclude:
 494            if item in url:
 495                if debug:
 496                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 497
 498                oK = False
 499                break
 500
 501        if oK:
 502            counter = 0
 503            response = None
 504            errMsg = ""
 505
 506            while not response and counter <= retry:
 507                if reqType == "GET":
 508                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 509
 510                if reqType == "POST":
 511                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 512
 513                if debug:
 514                    uLogger.debug("Response:")
 515                    uLogger.debug("    - status code: {}".format(response.status_code))
 516                    uLogger.debug("    - reason: {}".format(response.reason))
 517                    uLogger.debug("    - body length: {}".format(len(response.text)))
 518                    uLogger.debug("    - headers: {}".format(response.headers))
 519
 520                # Server returns some headers:
 521                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
 522                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
 523                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
 524                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 525                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 526                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 527                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 528                    sleep(rateLimitWait)
 529
 530                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 531                if 400 <= response.status_code < 500:
 532                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 533                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 534                    counter = retry + 1
 535
 536                if 500 <= response.status_code < 600:
 537                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 538                    uLogger.debug("    - not oK, {}".format(errMsg))
 539                    counter += 1
 540
 541                    if counter <= retry:
 542                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 543                        sleep(pause)
 544
 545            responseJSON = self._ParseJSON(response.text)
 546
 547            if errMsg:
 548                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 549                uLogger.error("    - not oK, {}".format(errMsg))
 550
 551        return responseJSON
 552
 553    def _IUpdater(self, iType: str) -> tuple:
 554        """
 555        Request instrument by type from server. See available API methods for instruments:
 556        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 557        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 558        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 559        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 560        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 561
 562        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 563        :return: tuple with iType name and list of available instruments of current type for defined user token.
 564        """
 565        result = []
 566
 567        if iType in TKS_INSTRUMENTS:
 568            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 569
 570            # all instruments have the same body in API v2 requests:
 571            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 572            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 573            result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"]
 574
 575        return iType, result
 576
 577    def _IWrapper(self, kwargs):
 578        """
 579        Wrapper runs instrument's update method `_IUpdater()`.
 580        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 581        """
 582        return self._IUpdater(**kwargs)
 583
 584    def Listing(self) -> dict:
 585        """
 586        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 587
 588        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 589        """
 590        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 591        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 592
 593        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 594        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 595        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 596
 597        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 598        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 599        poolUpdater.close()
 600
 601        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 602        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 603        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 604
 605        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 606        for iType in iList.keys():
 607            for ticker in iList[iType]:
 608                iList[iType][ticker]["type"] = iType
 609
 610                if "minPriceIncrement" in iList[iType][ticker].keys():
 611                    iList[iType][ticker]["step"] = NanoToFloat(
 612                        iList[iType][ticker]["minPriceIncrement"]["units"],
 613                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 614                    )
 615
 616                else:
 617                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 618
 619        return iList
 620
 621    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 622        """
 623        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 624
 625        See also: `DumpInstruments()`, `Listing()`.
 626
 627        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 628                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 629        """
 630        if self.iListDumpFile is None or not self.iListDumpFile:
 631            uLogger.error("Output name of dump file must be defined!")
 632            raise Exception("Filename required")
 633
 634        if not self.iList or forceUpdate:
 635            self.iList = self.Listing()
 636
 637        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 638
 639        # Save as XLSX with separated sheets for every type of instruments:
 640        with pd.ExcelWriter(
 641                path=xlsxDumpFile,
 642                date_format=TKS_DATE_FORMAT,
 643                datetime_format=TKS_DATE_TIME_FORMAT,
 644                mode="w",
 645        ) as writer:
 646            for iType in TKS_INSTRUMENTS:
 647                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 648                df = df[sorted(df)]  # sorted by column names
 649                df = df.applymap(
 650                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 651                    na_action="ignore",
 652                )  # converting numbers from nano-type to float in every cell
 653                df.to_excel(
 654                    writer,
 655                    sheet_name=iType,
 656                    encoding="UTF-8",
 657                    freeze_panes=(1, 1),
 658                )  # saving as XLSX-file with freeze first row and column as headers
 659
 660        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 661
 662    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 663        """
 664        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 665        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 666
 667        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 668
 669        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 670                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 671        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 672        """
 673        if self.iListDumpFile is None or not self.iListDumpFile:
 674            uLogger.error("Output name of dump file must be defined!")
 675            raise Exception("Filename required")
 676
 677        if not self.iList or forceUpdate:
 678            self.iList = self.Listing()
 679
 680        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 681        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 682            fH.write(jsonDump)
 683
 684        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 685
 686        return jsonDump
 687
 688    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 689        """
 690        Show information about one instrument defined by json data and prints it in Markdown format.
 691
 692        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 693
 694        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 695        :param show: if `True` then also printing information about instrument and its current price.
 696        :return: multilines text in Markdown format with information about one instrument.
 697        """
 698        splitLine = "|                                                             |                                                        |\n"
 699        infoText = ""
 700
 701        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 702            info = [
 703                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 704                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 705                "| Parameters                                                  | Values                                                 |\n",
 706                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 707                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 708                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 709            ]
 710
 711            if "sector" in iJSON.keys() and iJSON["sector"]:
 712                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 713
 714            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
 715                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
 716                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
 717            )))
 718
 719            info.extend([
 720                splitLine,
 721                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 722                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
 723            ])
 724
 725            if "isin" in iJSON.keys() and iJSON["isin"]:
 726                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 727
 728            if "classCode" in iJSON.keys():
 729                info.append("| Class Code:                                                 | {:<54} |\n".format(iJSON["classCode"]))
 730
 731            info.extend([
 732                splitLine,
 733                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 734                splitLine,
 735                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 736                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 737                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 738            ])
 739
 740            if iJSON["figi"]:
 741                self.figi = iJSON["figi"]
 742                iJSON = iJSON | self.RequestTradingStatus()
 743
 744                info.extend([
 745                    splitLine,
 746                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 747                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 748                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 749                ])
 750
 751            info.append(splitLine)
 752
 753            if "type" in iJSON.keys() and iJSON["type"]:
 754                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 755
 756            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 757                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 758
 759            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 760                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 761
 762            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 763                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 764
 765            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 766                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 767
 768            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 769                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 770
 771            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 772                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 773
 774            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 775                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 776
 777            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 778                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 779
 780            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 781                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 782
 783            if "currency" in iJSON.keys():
 784                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 785
 786            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 787                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 788
 789            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 790                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 791
 792            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 793                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 794
 795            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 796                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 797
 798            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 799                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 800
 801            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 802                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 803
 804            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 805                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 806
 807            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 808                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 809
 810            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 811                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 812
 813            iExt = None
 814            if iJSON["type"] == "Bonds":
 815                info.extend([
 816                    splitLine,
 817                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 818                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 819                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 820                        iJSON["nominal"]["currency"],
 821                    )),
 822                ])
 823
 824                if "floatingCouponFlag" in iJSON.keys():
 825                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 826
 827                if "amortizationFlag" in iJSON.keys():
 828                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 829
 830                info.append(splitLine)
 831
 832                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 833                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 834
 835                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 836
 837                info.extend([
 838                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 839                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 840                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 841                ])
 842
 843                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 844                    info.append("| Current Accrued Interest (ACI):                             | {:<54} |\n".format("{:.2f} {}".format(
 845                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 846                        iJSON["aciValue"]["currency"]
 847                    )))
 848
 849            if "currentPrice" in iJSON.keys():
 850                info.append(splitLine)
 851
 852                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 853                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 854
 855                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 856                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 857                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 858                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 859                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 860
 861                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 862                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 863
 864                info.extend([
 865                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 866                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 867                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 868                    )),
 869                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 870                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 871                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 872                    )),
 873                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 874                        "{:.2f}%{}".format(
 875                            iJSON["currentPrice"]["changes"],
 876                            " ({}{:.2f} {})".format(
 877                                "+" if bondChangesDelta > 0 else "",
 878                                bondChangesDelta,
 879                                aciCurrency
 880                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 881                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 882                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 883                                currency
 884                            ),
 885                        )
 886                    ),
 887                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 888                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 889                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 890                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 891                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 892                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 893                    )),
 894                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 895                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 896                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 897                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 898                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 899                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 900                    )),
 901                ])
 902
 903            if "lot" in iJSON.keys():
 904                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 905
 906            if "step" in iJSON.keys() and iJSON["step"] != 0:
 907                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
 908
 909            # Add bond payment calendar:
 910            if iJSON["type"] == "Bonds":
 911                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 912                info.extend(["\n", strCalendar])
 913
 914            infoText += "".join(info)
 915
 916            if show:
 917                uLogger.info("{}".format(infoText))
 918
 919            else:
 920                uLogger.debug("{}".format(infoText))
 921
 922            if self.infoFile is not None:
 923                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 924                    fH.write(infoText)
 925
 926                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 927
 928        return infoText
 929
 930    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
 931        """
 932        Search and return raw broker's information about instrument by its ticker.
 933        `ticker` must be defined! If debug=True then print all debug messages.
 934
 935        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 936        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 937        :param debug: if `True` then print all debug console messages.
 938        :return: JSON formatted data with information about instrument.
 939        """
 940        tickerJSON = {}
 941        if debug:
 942            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 943
 944        if not self.ticker:
 945            uLogger.warning("self.ticker variable is not be empty!")
 946
 947        else:
 948            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 949                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 950                raise Exception("Instrument not allowed")
 951
 952            if not self.iList:
 953                self.iList = self.Listing()
 954
 955            if self.ticker in self.iList["Shares"].keys():
 956                tickerJSON = self.iList["Shares"][self.ticker]
 957                if debug:
 958                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 959
 960            elif self.ticker in self.iList["Currencies"].keys():
 961                tickerJSON = self.iList["Currencies"][self.ticker]
 962                if debug:
 963                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 964
 965            elif self.ticker in self.iList["Bonds"].keys():
 966                tickerJSON = self.iList["Bonds"][self.ticker]
 967                if debug:
 968                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 969
 970            elif self.ticker in self.iList["Etfs"].keys():
 971                tickerJSON = self.iList["Etfs"][self.ticker]
 972                if debug:
 973                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 974
 975            elif self.ticker in self.iList["Futures"].keys():
 976                tickerJSON = self.iList["Futures"][self.ticker]
 977                if debug:
 978                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 979
 980        if tickerJSON:
 981            self.figi = tickerJSON["figi"]
 982
 983            if requestPrice:
 984                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 985
 986                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 987                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 988
 989                else:
 990                    tickerJSON["currentPrice"]["changes"] = 0
 991
 992            if show:
 993                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 994
 995        else:
 996            if show:
 997                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 998
 999        return tickerJSON
1000
1001    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1002        """
1003        Search and return raw broker's information about instrument by its FIGI.
1004        `figi` must be defined! If debug=True then print all debug messages.
1005
1006        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1007        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1008        :param debug: if `True` then print all debug console messages.
1009        :return: JSON formatted data with information about instrument.
1010        """
1011        figiJSON = {}
1012        if debug:
1013            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1014
1015        if not self.figi:
1016            uLogger.warning("self.figi variable is not be empty!")
1017
1018        else:
1019            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1020                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1021                raise Exception("Instrument not allowed")
1022
1023            if not self.iList:
1024                self.iList = self.Listing()
1025
1026            for item in self.iList["Shares"].keys():
1027                if self.figi == self.iList["Shares"][item]["figi"]:
1028                    figiJSON = self.iList["Shares"][item]
1029
1030                    if debug:
1031                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1032
1033                    break
1034
1035            if not figiJSON:
1036                for item in self.iList["Currencies"].keys():
1037                    if self.figi == self.iList["Currencies"][item]["figi"]:
1038                        figiJSON = self.iList["Currencies"][item]
1039
1040                        if debug:
1041                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1042
1043                        break
1044
1045            if not figiJSON:
1046                for item in self.iList["Bonds"].keys():
1047                    if self.figi == self.iList["Bonds"][item]["figi"]:
1048                        figiJSON = self.iList["Bonds"][item]
1049
1050                        if debug:
1051                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1052
1053                        break
1054
1055            if not figiJSON:
1056                for item in self.iList["Etfs"].keys():
1057                    if self.figi == self.iList["Etfs"][item]["figi"]:
1058                        figiJSON = self.iList["Etfs"][item]
1059
1060                        if debug:
1061                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1062
1063                        break
1064
1065            if not figiJSON:
1066                for item in self.iList["Futures"].keys():
1067                    if self.figi == self.iList["Futures"][item]["figi"]:
1068                        figiJSON = self.iList["Futures"][item]
1069
1070                        if debug:
1071                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1072
1073                        break
1074
1075        if figiJSON:
1076            self.figi = figiJSON["figi"]
1077            self.ticker = figiJSON["ticker"]
1078
1079            if requestPrice:
1080                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1081
1082                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1083                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1084
1085                else:
1086                    figiJSON["currentPrice"]["changes"] = 0
1087
1088            if show:
1089                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1090
1091        else:
1092            if show:
1093                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1094
1095        return figiJSON
1096
1097    def GetCurrentPrices(self, show: bool = True) -> dict:
1098        """
1099        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1100        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1101
1102        See also:
1103
1104        :param show: if `True` then print DOM to log and console.
1105        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1106        """
1107        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1108
1109        if self.depth < 1:
1110            uLogger.error("Depth of Market (DOM) must be >=1!")
1111            raise Exception("Incorrect value")
1112
1113        if not (self.ticker or self.figi):
1114            uLogger.error("self.ticker or self.figi variables must be defined!")
1115            raise Exception("Ticker or FIGI required")
1116
1117        if self.ticker and not self.figi:
1118            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1119            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1120
1121        if not self.ticker and self.figi:
1122            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1123            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1124
1125        if not self.figi:
1126            uLogger.error("FIGI is not defined!")
1127            raise Exception("Ticker or FIGI required")
1128
1129        else:
1130            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1131
1132            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1133            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1134            self.body = str({"figi": self.figi, "depth": self.depth})
1135            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1136
1137            if pricesResponse:
1138                # list of dicts with sellers orders:
1139                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1140
1141                # list of dicts with buyers orders:
1142                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1143
1144                # max price of instrument at this time:
1145                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1146
1147                # min price of instrument at this time:
1148                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1149
1150                # last price of deal with instrument:
1151                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1152
1153                # last close price of instrument:
1154                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1155
1156            else:
1157                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1158                uLogger.debug("Server response: {}".format(pricesResponse))
1159
1160            if show:
1161                if prices["buy"] or prices["sell"]:
1162                    info = [
1163                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1164                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1165                            self.ticker,
1166                            self.figi,
1167                            self.depth,
1168                        ),
1169                        uLog.sepShort, "\n",
1170                        " Orders of Buyers   | Orders of Sellers\n",
1171                        uLog.sepShort, "\n",
1172                        " Sell prices (vol.) | Buy prices (vol.)\n",
1173                        uLog.sepShort, "\n",
1174                    ]
1175
1176                    if not prices["buy"]:
1177                        info.append("                    | No orders!\n")
1178                        sumBuy = 0
1179
1180                    else:
1181                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1182                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1183                        for item in maxMinSorted:
1184                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1185
1186                    if not prices["sell"]:
1187                        info.append("No orders!          |\n")
1188                        sumSell = 0
1189
1190                    else:
1191                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1192                        for item in prices["sell"]:
1193                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1194
1195                    info.extend([
1196                        uLog.sepShort, "\n",
1197                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1198                        uLog.sepShort, "\n",
1199                    ])
1200
1201                    infoText = "".join(info)
1202
1203                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1204
1205                else:
1206                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1207
1208        return prices
1209
1210    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1211        """
1212        This method get and show information about all available broker instruments for current user account.
1213        If `instrumentsFile` string is not empty then also save information to this file.
1214
1215        :param show: if `True` then print results to console, if `False` - print only to file.
1216        :return: multi-lines string with all available broker instruments
1217        """
1218        if not self.iList:
1219            self.iList = self.Listing()
1220
1221        info = [
1222            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1223            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1224        ]
1225
1226        # add instruments count by type:
1227        for iType in self.iList.keys():
1228            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1229
1230        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1231        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1232
1233        # generating info tables with all instruments by type:
1234        for iType in self.iList.keys():
1235            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1236
1237            for instrument in self.iList[iType].keys():
1238                iName = self.iList[iType][instrument]["name"]  # instrument's name
1239                if len(iName) > 57:
1240                    iName = "{}...".format(iName[:54])  # right trim for a long string
1241
1242                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1243                    self.iList[iType][instrument]["ticker"],
1244                    iName,
1245                    self.iList[iType][instrument]["figi"],
1246                    self.iList[iType][instrument]["currency"],
1247                    self.iList[iType][instrument]["lot"],
1248                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1249                ))
1250
1251        infoText = "".join(info)
1252
1253        if show:
1254            uLogger.info(infoText)
1255
1256        if self.instrumentsFile:
1257            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1258                fH.write(infoText)
1259
1260            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1261
1262        return infoText
1263
1264    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1265        """
1266        This method search and show information about instruments by part of its ticker, FIGI or name.
1267        If `searchResultsFile` string is not empty then also save information to this file.
1268
1269        :param pattern: string with part of ticker, FIGI or instrument's name.
1270        :param show: if `True` then print results to console, if `False` - return list of result only.
1271        :return: list of dictionaries with all found instruments.
1272        """
1273        if not self.iList:
1274            self.iList = self.Listing()
1275
1276        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1277        compiledPattern = re.compile(pattern, re.IGNORECASE)
1278
1279        for iType in self.iList:
1280            for instrument in self.iList[iType].values():
1281                searchResult = compiledPattern.search(" ".join(
1282                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1283                ))
1284
1285                if searchResult:
1286                    searchResults[iType][instrument["ticker"]] = instrument
1287
1288        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1289        info = [
1290            "# Search results\n\n",
1291            "* **Search pattern:** [{}]\n".format(pattern),
1292            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1293            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1294        ]
1295        infoShort = info[:]
1296
1297        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1298        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1299        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1300
1301        if resultsLen == 0:
1302            info.append("\nNo results\n")
1303            infoShort.append("\nNo results\n")
1304            uLogger.warning("No results. Try changing your search pattern.")
1305
1306        else:
1307            for iType in searchResults:
1308                iTypeValuesCount = len(searchResults[iType].values())
1309                if iTypeValuesCount > 0:
1310                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1312
1313                    for instrument in searchResults[iType].values():
1314                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1315                            instrument["type"],
1316                            instrument["ticker"],
1317                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1318                            instrument["figi"],
1319                        ))
1320
1321                    if iTypeValuesCount <= 5:
1322                        infoShort.extend(info[-iTypeValuesCount:])
1323
1324                    else:
1325                        infoShort.extend(info[-5:])
1326                        infoShort.append(skippedLine)
1327
1328        infoText = "".join(info)
1329        infoTextShort = "".join(infoShort)
1330
1331        if show:
1332            uLogger.info(infoTextShort)
1333            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1334
1335        if self.searchResultsFile:
1336            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1337                fH.write(infoText)
1338
1339            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1340
1341        return searchResults
1342
1343    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1344        """
1345        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1346
1347        :param instruments: list of strings with tickers or FIGIs.
1348        :return: list with unique instrument FIGIs only.
1349        """
1350        requestedInstruments = []
1351        for iName in instruments:
1352            if iName not in self.aliases.keys():
1353                if iName not in requestedInstruments:
1354                    requestedInstruments.append(iName)
1355
1356            else:
1357                if iName not in requestedInstruments:
1358                    if self.aliases[iName] not in requestedInstruments:
1359                        requestedInstruments.append(self.aliases[iName])
1360
1361        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1362
1363        onlyUniqueFIGIs = []
1364        for iName in requestedInstruments:
1365            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1366                continue
1367
1368            self.ticker = iName
1369            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1370
1371            if not iData:
1372                self.ticker = ""
1373                self.figi = iName
1374
1375                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1376
1377                if not iData:
1378                    self.figi = ""
1379                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1380
1381            if iData and iData["figi"] not in onlyUniqueFIGIs:
1382                onlyUniqueFIGIs.append(iData["figi"])
1383
1384        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1385
1386        return onlyUniqueFIGIs
1387
1388    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1389        """
1390        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1391        See limits: https://tinkoff.github.io/investAPI/limits/
1392        If `pricesFile` string is not empty then also save information to this file.
1393
1394        :param instruments: list of strings with tickers or FIGIs.
1395        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1396        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1397                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1398        """
1399        if instruments is None or not instruments:
1400            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1401            raise Exception("Ticker or FIGI required")
1402
1403        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1404
1405        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1406
1407        iList = []  # trying to get info and current prices about all unique instruments:
1408        for self.figi in onlyUniqueFIGIs:
1409            iData = self.SearchByFIGI(requestPrice=True)
1410            iList.append(iData)
1411
1412        self.ShowListOfPrices(iList, show)
1413
1414        return iList
1415
1416    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1417        """
1418        Show table contains current prices of given instruments.
1419
1420        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1421                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1422        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1423        :return: multilines text in Markdown format as a table contains current prices.
1424        """
1425        infoText = ""
1426
1427        if show or self.pricesFile:
1428            info = [
1429                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1430                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1431                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1432            ]
1433
1434            for item in iList:
1435                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1436                    item["ticker"],
1437                    item["figi"],
1438                    item["type"],
1439                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1440                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1441                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1442                    "{} / {}".format(
1443                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1444                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1445                    ),
1446                    "{} / {}".format(
1447                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1448                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1449                    ),
1450                    item["currency"],
1451                ))
1452
1453            infoText = "".join(info)
1454
1455            if show:
1456                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1457
1458            if self.pricesFile:
1459                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1460                    fH.write(infoText)
1461
1462                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1463
1464        return infoText
1465
1466    def RequestTradingStatus(self) -> dict:
1467        """
1468        Requesting trading status for the instrument defined by `figi` variable.
1469        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1470        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1471
1472        :return: dictionary with trading status attributes. Response example:
1473                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1474                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1475        """
1476        if self.figi is None or not self.figi:
1477            uLogger.error("Variable `figi` must be defined for using this method!")
1478            raise Exception("FIGI required")
1479
1480        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1481
1482        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1483        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1484        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1485
1486        uLogger.debug("Records about current trading status successfully received")
1487
1488        return tradingStatus
1489
1490    def RequestPortfolio(self) -> dict:
1491        """
1492        Requesting actual user's portfolio for current `accountId`.
1493        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1494        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1495
1496        :return: dictionary with user's portfolio.
1497        """
1498        if self.accountId is None or not self.accountId:
1499            uLogger.error("Variable `accountId` must be defined for using this method!")
1500            raise Exception("Account ID required")
1501
1502        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1503
1504        self.body = str({"accountId": self.accountId})
1505        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1506        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1507
1508        uLogger.debug("Records about user's portfolio successfully received")
1509
1510        return rawPortfolio
1511
1512    def RequestPositions(self) -> dict:
1513        """
1514        Requesting open positions by currencies and instruments for current `accountId`.
1515        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1516        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1517
1518        :return: dictionary with open positions by instruments.
1519        """
1520        if self.accountId is None or not self.accountId:
1521            uLogger.error("Variable `accountId` must be defined for using this method!")
1522            raise Exception("Account ID required")
1523
1524        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1525
1526        self.body = str({"accountId": self.accountId})
1527        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1528        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1529
1530        uLogger.debug("Records about current open positions successfully received")
1531
1532        return rawPositions
1533
1534    def RequestPendingOrders(self) -> list:
1535        """
1536        Requesting current actual pending orders for current `accountId`.
1537        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1538        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1539
1540        :return: list of dictionaries with pending orders.
1541        """
1542        if self.accountId is None or not self.accountId:
1543            uLogger.error("Variable `accountId` must be defined for using this method!")
1544            raise Exception("Account ID required")
1545
1546        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1547
1548        self.body = str({"accountId": self.accountId})
1549        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1550        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1551
1552        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1553
1554        return rawOrders
1555
1556    def RequestStopOrders(self) -> list:
1557        """
1558        Requesting current actual stop orders for current `accountId`.
1559        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1560        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1561
1562        :return: list of dictionaries with stop orders.
1563        """
1564        if self.accountId is None or not self.accountId:
1565            uLogger.error("Variable `accountId` must be defined for using this method!")
1566            raise Exception("Account ID required")
1567
1568        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1569
1570        self.body = str({"accountId": self.accountId})
1571        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1572        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1573
1574        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1575
1576        return rawStopOrders
1577
1578    def Overview(self, show: bool = False, details: str = "full") -> dict:
1579        """
1580        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1581        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1582        are defined then also save information to file.
1583
1584        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1585        many requests about the state of the portfolio, and then, based on the received data, a large number
1586        of calculation and statistics are collected.
1587
1588        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1589        :param details: how detailed should the information be? You should specify one of strings:
1590                        `full` - shows full available information about portfolio status (by default),
1591                        `positions` - shows only open positions,
1592                        `digest` - show a short digest of the portfolio status,
1593                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1594                        `orders` - shows only sections of open limits and stop orders.
1595        :return: dictionary with client's raw portfolio and some statistics.
1596        """
1597        if self.accountId is None or not self.accountId:
1598            uLogger.error("Variable `accountId` must be defined for using this method!")
1599            raise Exception("Account ID required")
1600
1601        view = {
1602            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1603                "headers": {},  # list of dictionaries, response headers without "positions" section
1604                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1605                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1606                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1607                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1608                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1609                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1610                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1611                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1612                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1613            },
1614            "stat": {  # --- some statistics calculated using "raw" sections:
1615                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1616                "availableRUB": 0.,  # available rubles (without other currencies)
1617                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1618                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1619                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1620                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1621                "sharesCostRUB": 0.,  # costs of all shares in RUB
1622                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1623                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1624                "futuresCostRUB": 0.,  # costs of all futures in RUB
1625                "Currencies": [],  # list of dictionaries of all currencies statistics
1626                "Shares": [],  # list of dictionaries of all shares statistics
1627                "Bonds": [],  # list of dictionaries of all bonds statistics
1628                "Etfs": [],  # list of dictionaries of all etfs statistics
1629                "Futures": [],  # list of dictionaries of all futures statistics
1630                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1631                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1632                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1633                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1634                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1635            },
1636            "analytics": {  # --- some analytics of portfolio:
1637                "distrByAssets": {},  # portfolio distribution by assets
1638                "distrByCompanies": {},  # portfolio distribution by companies
1639                "distrBySectors": {},  # portfolio distribution by sectors
1640                "distrByCurrencies": {},  # portfolio distribution by currencies
1641                "distrByCountries": {},  # portfolio distribution by countries
1642            }
1643        }
1644
1645        details = details.lower()
1646        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1647        if details not in availableDetails:
1648            details = "full"
1649            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1650
1651        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1652
1653        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1654        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1655        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1656        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1657
1658        # save response headers without "positions" section:
1659        for key in portfolioResponse.keys():
1660            if key != "positions":
1661                view["raw"]["headers"][key] = portfolioResponse[key]
1662
1663            else:
1664                continue
1665
1666        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1667        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1668        for item in portfolioResponse["positions"]:
1669            if item["instrumentType"] == "currency":
1670                self.figi = item["figi"]
1671                curr = self.SearchByFIGI(requestPrice=False)
1672
1673                # current price of currency in RUB:
1674                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1675                    "name": curr["name"],
1676                    "currentPrice": NanoToFloat(
1677                        item["currentPrice"]["units"],
1678                        item["currentPrice"]["nano"]
1679                    ),
1680                }
1681
1682                view["raw"]["Currencies"].append(item)
1683
1684            elif item["instrumentType"] == "share":
1685                view["raw"]["Shares"].append(item)
1686
1687            elif item["instrumentType"] == "bond":
1688                view["raw"]["Bonds"].append(item)
1689
1690            elif item["instrumentType"] == "etf":
1691                view["raw"]["Etfs"].append(item)
1692
1693            elif item["instrumentType"] == "futures":
1694                view["raw"]["Futures"].append(item)
1695
1696            else:
1697                continue
1698
1699        # how many volume of currencies (by ISO currency name) are blocked:
1700        for item in view["raw"]["positions"]["blocked"]:
1701            blocked = NanoToFloat(item["units"], item["nano"])
1702            if blocked > 0:
1703                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1704
1705        # how many volume of instruments (by FIGI) are blocked:
1706        for item in view["raw"]["positions"]["securities"]:
1707            blocked = int(item["blocked"])
1708            if blocked > 0:
1709                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1710
1711        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1712
1713        if "rub" in allBlocked.keys():
1714            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1715
1716        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1717        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1718        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1719        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1720        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1721        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1722        view["stat"]["portfolioCostRUB"] = sum([
1723            view["stat"]["allCurrenciesCostRUB"],
1724            view["stat"]["sharesCostRUB"],
1725            view["stat"]["bondsCostRUB"],
1726            view["stat"]["etfsCostRUB"],
1727            view["stat"]["futuresCostRUB"],
1728        ])
1729
1730        # --- calculating some portfolio statistics:
1731        byComp = {}  # distribution by companies
1732        bySect = {}  # distribution by sectors
1733        byCurr = {}  # distribution by currencies (include RUB)
1734        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1735        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1736
1737        for item in portfolioResponse["positions"]:
1738            self.figi = item["figi"]
1739            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1740
1741            if instrument:
1742                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1743                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1744
1745                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1746                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1747
1748                else:
1749                    blocked = 0
1750
1751                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1752                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1753                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1754                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1755                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1756                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1757                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1758                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1759                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1760                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1761                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1762                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1763
1764                statData = {
1765                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1766                    "ticker": instrument["ticker"],  # ticker by FIGI
1767                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1768                    "volume": volume,  # available volume of instrument
1769                    "lots": lots,  # volume in lots of instrument
1770                    "direction": direction,  # direction of an instrument's position: short or long
1771                    "blocked": blocked,  # blocked volume of currency or instrument
1772                    "currentPrice": curPrice,  # current instrument's price in basic asset
1773                    "average": average,  # current average position price
1774                    "cost": cost,  # current cost of all volume of instrument in basic asset
1775                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1776                    "costRUB": costRUB,  # cost of instrument in ruble
1777                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1778                    "profit": profit,  # expected profit at current moment
1779                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1780                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1781                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1782                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1783                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1784                    "step": instrument["step"],  # minimum price increment
1785                }
1786
1787                # adding distribution by unique countries:
1788                if statData["country"] not in byCountry.keys():
1789                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1790
1791                else:
1792                    byCountry[statData["country"]]["cost"] += costRUB
1793                    byCountry[statData["country"]]["percent"] += percentCostRUB
1794
1795                if item["instrumentType"] != "currency":
1796                    # adding distribution by unique companies:
1797                    if statData["name"]:
1798                        if statData["name"] not in byComp.keys():
1799                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1800
1801                        else:
1802                            byComp[statData["name"]]["cost"] += costRUB
1803                            byComp[statData["name"]]["percent"] += percentCostRUB
1804
1805                    # adding distribution by unique sectors:
1806                    if statData["sector"] not in bySect.keys():
1807                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1808
1809                    else:
1810                        bySect[statData["sector"]]["cost"] += costRUB
1811                        bySect[statData["sector"]]["percent"] += percentCostRUB
1812
1813                # adding distribution by unique currencies:
1814                if currency not in byCurr.keys():
1815                    byCurr[currency] = {
1816                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1817                        "cost": costRUB,
1818                        "percent": percentCostRUB
1819                    }
1820
1821                else:
1822                    byCurr[currency]["cost"] += costRUB
1823                    byCurr[currency]["percent"] += percentCostRUB
1824
1825                # saving statistics for every instrument:
1826                if item["instrumentType"] == "currency":
1827                    view["stat"]["Currencies"].append(statData)
1828
1829                    # update dict with free funds for trading (total - blocked) by currencies
1830                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1831                    view["stat"]["funds"][currency] = {
1832                        "total": volume,
1833                        "totalCostRUB": costRUB,  # total volume cost in rubles
1834                        "free": volume - blocked,
1835                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1836                    }
1837
1838                elif item["instrumentType"] == "share":
1839                    view["stat"]["Shares"].append(statData)
1840
1841                elif item["instrumentType"] == "bond":
1842                    view["stat"]["Bonds"].append(statData)
1843
1844                elif item["instrumentType"] == "etf":
1845                    view["stat"]["Etfs"].append(statData)
1846
1847                elif item["instrumentType"] == "Futures":
1848                    view["stat"]["Futures"].append(statData)
1849
1850                else:
1851                    continue
1852
1853        # total changes in Russian Ruble:
1854        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1855        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1856        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1857        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1858        view["stat"]["funds"]["rub"] = {
1859            "total": view["stat"]["availableRUB"],
1860            "totalCostRUB": view["stat"]["availableRUB"],
1861            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1862            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1863        }
1864
1865        # --- pending orders sector data:
1866        uniquePendingOrders = []
1867        uniquePendingOrdersFIGIs = []
1868        for item in view["raw"]["orders"]:
1869            if item["figi"] not in uniquePendingOrdersFIGIs:
1870                uniquePendingOrdersFIGIs.append(item["figi"])
1871                uniquePendingOrders.append(item)
1872
1873        for item in uniquePendingOrders:
1874            self.figi = item["figi"]
1875            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1876
1877            if instrument:
1878                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1879                orderType = TKS_ORDER_TYPES[item["orderType"]]
1880                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1881                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1882
1883                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1884                if item["direction"] == "ORDER_DIRECTION_BUY":
1885                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1886
1887                else:
1888                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1889
1890                # requested price for order execution:
1891                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1892
1893                # necessary changes in percent to reach target from current price:
1894                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1895
1896                view["stat"]["orders"].append({
1897                    "orderID": item["orderId"],  # orderId number parameter of current order
1898                    "figi": item["figi"],  # FIGI identification
1899                    "ticker": instrument["ticker"],  # ticker name by FIGI
1900                    "lotsRequested": item["lotsRequested"],  # requested lots value
1901                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1902                    "currentPrice": lastPrice,  # current instrument's price for defined action
1903                    "targetPrice": target,  # requested price for order execution in base currency
1904                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1905                    "percentChanges": changes,  # changes in percent to target from current price
1906                    "currency": item["currency"],  # instrument's currency name
1907                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1908                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1909                    "status": orderState,  # order status from TKS_ORDER_STATES
1910                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1911                })
1912
1913        # --- stop orders sector data:
1914        uniqueStopOrders = []
1915        uniqueStopOrdersFIGIs = []
1916        for item in view["raw"]["stopOrders"]:
1917            if item["figi"] not in uniqueStopOrdersFIGIs:
1918                uniqueStopOrdersFIGIs.append(item["figi"])
1919                uniqueStopOrders.append(item)
1920
1921        for item in uniqueStopOrders:
1922            self.figi = item["figi"]
1923            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1924
1925            if instrument:
1926                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1927                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1928                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1929
1930                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1931                if "expirationTime" in item.keys():
1932                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1933                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1934
1935                else:
1936                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1937                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1938
1939                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1940                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1941                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1942
1943                else:
1944                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1945
1946                # requested price when stop-order executed:
1947                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1948
1949                # price for limit-order, set up when stop-order executed:
1950                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1951
1952                # necessary changes in percent to reach target from current price:
1953                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1954
1955                view["stat"]["stopOrders"].append({
1956                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1957                    "figi": item["figi"],  # FIGI identification
1958                    "ticker": instrument["ticker"],  # ticker name by FIGI
1959                    "lotsRequested": item["lotsRequested"],  # requested lots value
1960                    "currentPrice": lastPrice,  # current instrument's price for defined action
1961                    "targetPrice": target,  # requested price for stop-order execution in base currency
1962                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1963                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1964                    "percentChanges": changes,  # changes in percent to target from current price
1965                    "currency": item["currency"],  # instrument's currency name
1966                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1967                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1968                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1969                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1970                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1971                })
1972
1973        # --- calculating data for analytics section:
1974        # portfolio distribution by assets:
1975        view["analytics"]["distrByAssets"] = {
1976            "Ruble": {
1977                "uniques": 1,
1978                "cost": view["stat"]["availableRUB"],
1979                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1980            },
1981            "Currencies": {
1982                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1983                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1984                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1985            },
1986            "Shares": {
1987                "uniques": len(view["stat"]["Shares"]),
1988                "cost": view["stat"]["sharesCostRUB"],
1989                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1990            },
1991            "Bonds": {
1992                "uniques": len(view["stat"]["Bonds"]),
1993                "cost": view["stat"]["bondsCostRUB"],
1994                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1995            },
1996            "Etfs": {
1997                "uniques": len(view["stat"]["Etfs"]),
1998                "cost": view["stat"]["etfsCostRUB"],
1999                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2000            },
2001            "Futures": {
2002                "uniques": len(view["stat"]["Futures"]),
2003                "cost": view["stat"]["futuresCostRUB"],
2004                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2005            },
2006        }
2007
2008        # portfolio distribution by companies:
2009        view["analytics"]["distrByCompanies"]["All money cash"] = {
2010            "ticker": "",
2011            "cost": view["stat"]["allCurrenciesCostRUB"],
2012            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2013        }
2014        view["analytics"]["distrByCompanies"].update(byComp)
2015
2016        # portfolio distribution by sectors:
2017        view["analytics"]["distrBySectors"]["All money cash"] = {
2018            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2019            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2020        }
2021        view["analytics"]["distrBySectors"].update(bySect)
2022
2023        # portfolio distribution by currencies:
2024        view["analytics"]["distrByCurrencies"].update(byCurr)
2025        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2026        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2027
2028        # portfolio distribution by countries:
2029        view["analytics"]["distrByCountries"].update(byCountry)
2030
2031        # --- Prepare text statistics overview in human-readable:
2032        if show:
2033            # Whatever the value `details`, header not changes:
2034            info = [
2035                "# Client's portfolio\n\n",
2036                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2037                "* **Account ID:** [{}]\n".format(self.accountId),
2038            ]
2039
2040            if details in ["full", "positions", "digest"]:
2041                info.extend([
2042                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2043                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2044                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2045                        view["stat"]["totalChangesRUB"],
2046                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2047                        view["stat"]["totalChangesPercentRUB"],
2048                    ),
2049                ])
2050
2051            if details in ["full", "positions"]:
2052                info.extend([
2053                    "## Open positions\n\n",
2054                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2055                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2056                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2057                        "{:.2f} ({:.2f}) rub".format(
2058                            view["stat"]["availableRUB"],
2059                            view["stat"]["blockedRUB"],
2060                        )
2061                    )
2062                ])
2063
2064                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2065                    return [
2066                        "|                             |                                 |          |              |              |                     |                              |\n",
2067                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2068                            noTradeStr if noTradeStr else typeStr,
2069                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2070                        ),
2071                    ]
2072
2073                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2074                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2075                        "{} [{}]".format(data["ticker"], data["figi"]),
2076                        "{:.2f} ({:.2f}) {}".format(
2077                            data["volume"],
2078                            data["blocked"],
2079                            data["currency"],
2080                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2081                            data["volume"],
2082                            data["blocked"],
2083                        ),
2084                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2085                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2086                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2087                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2088                        "{}{:.2f} {} ({}{:.2f}%)".format(
2089                            "+" if data["profit"] > 0 else "",
2090                            data["profit"], data["baseCurrencyName"],
2091                            "+" if data["percentProfit"] > 0 else "",
2092                            data["percentProfit"],
2093                        ),
2094                    )
2095
2096                # --- Show currencies section:
2097                if view["stat"]["Currencies"]:
2098                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2099                    for item in view["stat"]["Currencies"]:
2100                        info.append(_InfoStr(item, showCurrencyName=True))
2101
2102                else:
2103                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2104
2105                # --- Show shares section:
2106                if view["stat"]["Shares"]:
2107                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2108
2109                    for item in view["stat"]["Shares"]:
2110                        info.append(_InfoStr(item))
2111
2112                else:
2113                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2114
2115                # --- Show bonds section:
2116                if view["stat"]["Bonds"]:
2117                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2118
2119                    for item in view["stat"]["Bonds"]:
2120                        info.append(_InfoStr(item))
2121
2122                else:
2123                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2124
2125                # --- Show etfs section:
2126                if view["stat"]["Etfs"]:
2127                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2128
2129                    for item in view["stat"]["Etfs"]:
2130                        info.append(_InfoStr(item))
2131
2132                else:
2133                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2134
2135                # --- Show futures section:
2136                if view["stat"]["Futures"]:
2137                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2138
2139                    for item in view["stat"]["Futures"]:
2140                        info.append(_InfoStr(item))
2141
2142                else:
2143                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2144
2145            if details in ["full", "orders"]:
2146                # --- Show pending orders section:
2147                if view["stat"]["orders"]:
2148                    info.extend([
2149                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2150                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2151                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2152                    ])
2153
2154                    for item in view["stat"]["orders"]:
2155                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2156                            "{} [{}]".format(item["ticker"], item["figi"]),
2157                            item["orderID"],
2158                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2159                            "{} {} ({}{:.2f}%)".format(
2160                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2161                                item["baseCurrencyName"],
2162                                "+" if item["percentChanges"] > 0 else "",
2163                                float(item["percentChanges"]),
2164                            ),
2165                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2166                            item["action"],
2167                            item["type"],
2168                            item["date"],
2169                        ))
2170
2171                else:
2172                    info.append("\n## Total pending limit-orders: 0\n")
2173
2174                # --- Show stop orders section:
2175                if view["stat"]["stopOrders"]:
2176                    info.extend([
2177                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2178                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2179                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2180                    ])
2181
2182                    for item in view["stat"]["stopOrders"]:
2183                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2184                            "{} [{}]".format(item["ticker"], item["figi"]),
2185                            item["orderID"],
2186                            item["lotsRequested"],
2187                            "{} {} ({}{:.2f}%)".format(
2188                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2189                                item["baseCurrencyName"],
2190                                "+" if item["percentChanges"] > 0 else "",
2191                                float(item["percentChanges"]),
2192                            ),
2193                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2194                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2195                            item["action"],
2196                            item["type"],
2197                            item["expType"],
2198                            item["createDate"],
2199                            item["expDate"],
2200                        ))
2201
2202                else:
2203                    info.append("\n## Total stop-orders: 0\n")
2204
2205            if details in ["full", "analytics"]:
2206                # -- Show analytics section:
2207                if view["stat"]["portfolioCostRUB"] > 0:
2208                    info.extend([
2209                        "\n# Analytics\n"
2210                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2211                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2212                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2213                            view["stat"]["totalChangesRUB"],
2214                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2215                            view["stat"]["totalChangesPercentRUB"],
2216                        ),
2217                        "\n## Portfolio distribution by assets\n"
2218                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2219                        "|------------|---------|---------|--------------------|\n",
2220                    ])
2221
2222                    for key in view["analytics"]["distrByAssets"].keys():
2223                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2224                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2225                                key,
2226                                view["analytics"]["distrByAssets"][key]["uniques"],
2227                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2228                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2229                            ))
2230
2231                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2232                    info.extend([
2233                        "\n## Portfolio distribution by companies\n"
2234                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2235                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2236                    ])
2237
2238                    for company in view["analytics"]["distrByCompanies"].keys():
2239                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2240                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2241                            info.append("| {} | {:<7} | {:<18} |\n".format(
2242                                "{}{}{}".format(
2243                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2244                                    company,
2245                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2246                                ),
2247                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2248                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2249                            ))
2250
2251                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2252                    info.extend([
2253                        "\n## Portfolio distribution by sectors\n"
2254                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2255                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2256                    ])
2257
2258                    for sector in view["analytics"]["distrBySectors"].keys():
2259                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2260                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2261                                sector,
2262                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2263                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2264                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2265                            ))
2266
2267                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2268                    info.extend([
2269                        "\n## Portfolio distribution by currencies\n"
2270                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2271                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2272                    ])
2273
2274                    for curr in view["analytics"]["distrByCurrencies"].keys():
2275                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2276                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2277                            info.append("| {} | {:<7} | {:<18} |\n".format(
2278                                "[{}] {}{}".format(
2279                                    curr,
2280                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2281                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2282                                ),
2283                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2284                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2285                            ))
2286
2287                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2288                    info.extend([
2289                        "\n## Portfolio distribution by countries\n"
2290                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2291                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2292                    ])
2293
2294                    for country in view["analytics"]["distrByCountries"].keys():
2295                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2296                            nameLen = len(country)
2297                            info.append("| {} | {:<7} | {:<18} |\n".format(
2298                                "{}{}".format(
2299                                    country,
2300                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2301                                ),
2302                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2303                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2304                            ))
2305
2306            infoText = "".join(info)
2307
2308            uLogger.info(infoText)
2309
2310            if details == "full" and self.overviewFile:
2311                filename = self.overviewFile
2312
2313            elif details == "digest" and self.overviewDigestFile:
2314                filename = self.overviewDigestFile
2315
2316            elif details == "positions" and self.overviewPositionsFile:
2317                filename = self.overviewPositionsFile
2318
2319            elif details == "orders" and self.overviewOrdersFile:
2320                filename = self.overviewOrdersFile
2321
2322            elif details == "analytics" and self.overviewAnalyticsFile:
2323                filename = self.overviewAnalyticsFile
2324
2325            else:
2326                filename = ""
2327
2328            if filename:
2329                with open(filename, "w", encoding="UTF-8") as fH:
2330                    fH.write(infoText)
2331
2332                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2333
2334        return view
2335
2336    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2337        """
2338        Returns history operations between two given dates for current `accountId`.
2339        If `reportFile` string is not empty then also save human-readable report.
2340        Shows some statistical data of closed positions.
2341
2342        :param start: see docstring in `GetDatesAsString()` method
2343        :param end: see docstring in `GetDatesAsString()` method
2344        :param show: if `True` then also prints all records to the console.
2345        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2346        :return: original list of dictionaries with history of deals records from API ("operations" key):
2347                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2348                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2349        """
2350        if self.accountId is None or not self.accountId:
2351            uLogger.error("Variable `accountId` must be defined for using this method!")
2352            raise Exception("Account ID required")
2353
2354        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2355
2356        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2357
2358        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2359        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2360        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2361        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2362        customStat = {}  # custom statistics in additional to responseJSON
2363
2364        # --- output report in human-readable format:
2365        if show or self.reportFile:
2366            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2367            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2368            nextDay = ""
2369
2370            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2371
2372            if len(ops) > 0:
2373                customStat = {
2374                    "opsCount": 0,  # total operations count
2375                    "buyCount": 0,  # buy operations
2376                    "sellCount": 0,  # sell operations
2377                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2378                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2379                    "payIn": {"rub": 0.},  # Deposit brokerage account
2380                    "payOut": {"rub": 0.},  # Withdrawals
2381                    "divs": {"rub": 0.},  # Dividends income
2382                    "coupons": {"rub": 0.},  # Coupon's income
2383                    "brokerCom": {"rub": 0.},  # Service commissions
2384                    "serviceCom": {"rub": 0.},  # Service commissions
2385                    "marginCom": {"rub": 0.},  # Margin commissions
2386                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2387                }
2388
2389                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2390                for item in ops:
2391                    if item["state"] == "OPERATION_STATE_EXECUTED":
2392                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2393
2394                        # count buy operations:
2395                        if "_BUY" in item["operationType"]:
2396                            customStat["buyCount"] += 1
2397
2398                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2399                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2400
2401                            else:
2402                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2403
2404                        # count sell operations:
2405                        elif "_SELL" in item["operationType"]:
2406                            customStat["sellCount"] += 1
2407
2408                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2409                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2410
2411                            else:
2412                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2413
2414                        # count incoming operations:
2415                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2416                            if item["payment"]["currency"] in customStat["payIn"].keys():
2417                                customStat["payIn"][item["payment"]["currency"]] += payment
2418
2419                            else:
2420                                customStat["payIn"][item["payment"]["currency"]] = payment
2421
2422                        # count withdrawals operations:
2423                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2424                            if item["payment"]["currency"] in customStat["payOut"].keys():
2425                                customStat["payOut"][item["payment"]["currency"]] += payment
2426
2427                            else:
2428                                customStat["payOut"][item["payment"]["currency"]] = payment
2429
2430                        # count dividends income:
2431                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2432                            if item["payment"]["currency"] in customStat["divs"].keys():
2433                                customStat["divs"][item["payment"]["currency"]] += payment
2434
2435                            else:
2436                                customStat["divs"][item["payment"]["currency"]] = payment
2437
2438                        # count coupon's income:
2439                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2440                            if item["payment"]["currency"] in customStat["coupons"].keys():
2441                                customStat["coupons"][item["payment"]["currency"]] += payment
2442
2443                            else:
2444                                customStat["coupons"][item["payment"]["currency"]] = payment
2445
2446                        # count broker commissions:
2447                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2448                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2449                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2450
2451                            else:
2452                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2453
2454                        # count service commissions:
2455                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2456                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2457                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2458
2459                            else:
2460                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2461
2462                        # count margin commissions:
2463                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2464                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2465                                customStat["marginCom"][item["payment"]["currency"]] += payment
2466
2467                            else:
2468                                customStat["marginCom"][item["payment"]["currency"]] = payment
2469
2470                        # count withholding taxes:
2471                        elif "_TAX" in item["operationType"]:
2472                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2473                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2474
2475                            else:
2476                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2477
2478                        else:
2479                            continue
2480
2481                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2482
2483                # --- view "Actions" lines:
2484                info.extend([
2485                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2486                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2487                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2488                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2489                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2490                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2491                    ),
2492                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2493                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2494                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2495                    ),
2496                ])
2497
2498                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2499                for key in opsKeys:
2500                    if key == "rub":
2501                        continue
2502
2503                    info.extend([
2504                        "|                            |                               | {:<28} |                      |                        |\n".format(
2505                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2506                        ),
2507                        "|                            |                               | {:<28} |                      |                        |\n".format(
2508                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2509                        ),
2510                    ])
2511
2512                info.append(splitLine1)
2513
2514                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2515                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2516                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2517                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2518                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2519                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2520                    )
2521
2522                # --- view "Payments" lines:
2523                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2524                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2525
2526                for key in paymentsKeys:
2527                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2528
2529                info.append(splitLine1)
2530
2531                # --- view "Commissions and taxes" lines:
2532                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2533                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2534
2535                for key in comKeys:
2536                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2537
2538                info.append(splitLine1)
2539
2540                info.extend([
2541                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2542                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2543                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2544                ])
2545
2546            else:
2547                info.append("Broker returned no operations during this period\n")
2548
2549            # --- view "Operations" section:
2550            for item in ops:
2551                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2552                    continue
2553
2554                else:
2555                    self.figi = item["figi"] if item["figi"] else ""
2556                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2557                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2558
2559                    # group of deals during one day:
2560                    if nextDay and item["date"].split("T")[0] != nextDay:
2561                        info.append(splitLine2)
2562                        nextDay = ""
2563
2564                    else:
2565                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2566
2567                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2568                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2569                        self.figi if self.figi else "—",
2570                        instrument["ticker"] if instrument else "—",
2571                        instrument["type"] if instrument else "—",
2572                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2573                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2574                        TKS_OPERATION_STATES[item["state"]],
2575                        TKS_OPERATION_TYPES[item["operationType"]],
2576                    ))
2577
2578            infoText = "".join(info)
2579
2580            if show:
2581                uLogger.info(infoText)
2582
2583            if self.reportFile:
2584                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2585                    fH.write(infoText)
2586
2587                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2588
2589        return ops, customStat
2590
2591    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2592        """
2593        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2594
2595        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2596        Warning! Broker server used ISO UTC time by default.
2597
2598        If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe.
2599        Also, `historyFile` used to update history with `onlyMissing` parameter.
2600
2601        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2602
2603        :param start: see docstring in `GetDatesAsString()` method.
2604        :param end: see docstring in `GetDatesAsString()` method.
2605        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2606                         `"hour"`, `"day"`. Default: `"hour"`.
2607        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2608                            False by default. Warning! History appends only from last candle to current time
2609                            with always update last candle!
2610        :param csvSep: separator if csv-file is used, `,` by default.
2611        :param show: if `True` then also prints pandas dataframe to the console.
2612        :return: pandas dataframe with prices history. Headers of columns are defined by default:
2613                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2614        """
2615        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2616        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2617        history = None  # empty pandas object for history
2618
2619        if interval not in TKS_CANDLE_INTERVALS.keys():
2620            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2621            raise Exception("Incorrect value")
2622
2623        if not (self.ticker or self.figi):
2624            uLogger.error("Ticker or FIGI must be defined!")
2625            raise Exception("Ticker or FIGI required")
2626
2627        if self.ticker and not self.figi:
2628            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2629            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2630
2631        if self.figi and not self.ticker:
2632            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2633            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2634
2635        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2636        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2637        if interval.lower() != "day":
2638            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2639
2640        delta = dtEnd - dtStart  # current UTC time minus last time in file
2641        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2642
2643        # calculate history length in candles:
2644        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2645        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2646            length += 1  # to avoid fraction time
2647
2648        # calculate data blocks count:
2649        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2650
2651        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2652        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2653        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2654        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2655        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2656
2657        tempOld = None  # pandas object for old history, if --only-missing key present
2658        lastTime = None  # datetime object of last old candle in file
2659
2660        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2661            uLogger.debug("--only-missing key present, add only last missing candles...")
2662            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2663
2664            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2665
2666            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2667            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2668            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2669            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2670
2671            # get last datetime object from last string in file or minus 1 delta if file is empty:
2672            if len(tempOld) > 0:
2673                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2674
2675            else:
2676                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2677
2678            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2679
2680        responseJSONs = []  # raw history blocks of data
2681
2682        blockEnd = dtEnd
2683        for item in range(blocks):
2684            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2685            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2686
2687            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2688                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2689            ))
2690
2691            if blockStart == blockEnd:
2692                uLogger.debug("Skipped this zero-length block...")
2693
2694            else:
2695                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2696                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2697                self.body = str({
2698                    "figi": self.figi,
2699                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2700                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2701                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2702                })
2703                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2704
2705                if "code" in responseJSON.keys():
2706                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2707
2708                else:
2709                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2710                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2711
2712                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2713
2714            blockEnd = blockStart
2715
2716        printCount = len(responseJSONs)  # candles to show in console
2717        if responseJSONs:
2718            tempHistory = pd.DataFrame(
2719                data={
2720                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2721                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2722                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2723                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2724                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2725                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2726                    "volume": [int(item["volume"]) for item in responseJSONs],
2727                },
2728                index=range(len(responseJSONs)),
2729                columns=["date", "time", "open", "high", "low", "close", "volume"],
2730            )
2731            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2732            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2733
2734            # append only newest candles to old history if --only-missing key present:
2735            if onlyMissing and tempOld is not None and lastTime is not None:
2736                index = 0  # find start index in tempHistory data:
2737
2738                for i, item in tempHistory.iterrows():
2739                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2740
2741                    if curTime == lastTime:
2742                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2743                        index = i
2744                        printCount = index + 1
2745                        break
2746
2747                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2748
2749            else:
2750                history = tempHistory  # if no `--only-missing` key then load full data from server
2751
2752            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2753
2754        if history is not None and not history.empty:
2755            if show:
2756                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2757                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2758                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2759                ))
2760
2761        else:
2762            uLogger.warning("Received an empty candles history!")
2763
2764        if self.historyFile is not None:
2765            if history is not None and not history.empty:
2766                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2767                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2768
2769            else:
2770                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2771
2772        else:
2773            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.")
2774
2775        return history
2776
2777    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2778        """
2779        Load candles history from csv-file and return pandas dataframe object.
2780
2781        See also: `History()` and `ShowHistoryChart()` methods.
2782
2783        :param filePath: path to csv-file to open.
2784        """
2785        loadedHistory = None  # init candles data object
2786
2787        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2788
2789        if os.path.exists(filePath):
2790            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as pandas dataframe
2791
2792            tfStr = self.priceModel.FormattedDelta(
2793                self.priceModel.timeframe,
2794                "{days} days {hours}h {minutes}m {seconds}s",
2795            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2796                self.priceModel.timeframe,
2797                "{hours}h {minutes}m {seconds}s",
2798            )
2799
2800            if loadedHistory is not None and not loadedHistory.empty:
2801                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2802                    len(loadedHistory),
2803                    tfStr,
2804                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2805                )
2806
2807            else:
2808                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2809
2810        else:
2811            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2812
2813        return loadedHistory
2814
2815    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2816        """
2817        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2818
2819        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2820        Default: `index.html` (both for interact and non-interact candlesticks chart).
2821
2822        See also: `History()` and `LoadHistory()` methods.
2823
2824        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2825        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2826                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2827                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2828                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2829        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2830                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2831        """
2832        if isinstance(candles, str):
2833            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2834            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2835
2836        elif isinstance(candles, pd.DataFrame):
2837            self.priceModel.prices = candles  # set candles chain from variable
2838            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2839
2840            if "datetime" not in candles.columns:
2841                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2842
2843        else:
2844            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2845            raise Exception("Incorrect value")
2846
2847        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2848
2849        if interact:
2850            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2851
2852            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2853
2854        else:
2855            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2856
2857            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2858
2859        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2860
2861    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2862        """
2863        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2864        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2865
2866        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2867
2868        :param operation: string "Buy" or "Sell".
2869        :param lots: volume, integer count of lots >= 1.
2870        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2871        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2872        :param expDate: string "Undefined" by default or local date in future,
2873                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2874        :return: JSON with response from broker server.
2875        """
2876        if self.accountId is None or not self.accountId:
2877            uLogger.error("Variable `accountId` must be defined for using this method!")
2878            raise Exception("Account ID required")
2879
2880        if operation is None or not operation or operation not in ("Buy", "Sell"):
2881            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2882            raise Exception("Incorrect value")
2883
2884        if lots is None or lots < 1:
2885            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2886            lots = 1
2887
2888        if tp is None or tp < 0:
2889            tp = 0
2890
2891        if sl is None or sl < 0:
2892            sl = 0
2893
2894        if expDate is None or not expDate:
2895            expDate = "Undefined"
2896
2897        if not (self.ticker or self.figi):
2898            uLogger.error("Ticker or FIGI must be defined!")
2899            raise Exception("Ticker or FIGI required")
2900
2901        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2902        self.ticker = instrument["ticker"]
2903        self.figi = instrument["figi"]
2904
2905        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2906
2907        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2908        self.body = str({
2909            "figi": self.figi,
2910            "quantity": str(lots),
2911            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2912            "accountId": str(self.accountId),
2913            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2914        })
2915        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2916
2917        if "orderId" in response.keys():
2918            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2919                operation, response["orderId"],
2920                self.ticker, self.figi, lots,
2921                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2922                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2923                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2924            ))
2925
2926        else:
2927            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2928
2929        if tp > 0:
2930            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2931
2932        if sl > 0:
2933            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2934
2935        return response
2936
2937    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2938        """
2939        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2940        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2941
2942        See also: `Order()` and `Trade()` docstrings.
2943
2944        :param lots: volume, integer count of lots >= 1.
2945        :param tp: float > 0, take profit price of stop-order.
2946        :param sl: float > 0, stop loss price of stop-order.
2947        :param expDate: it's a local date in future.
2948                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2949        :return: JSON with response from broker server.
2950        """
2951        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2952
2953    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2954        """
2955        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2956        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2957
2958        See also: `Order()` and `Trade()` docstrings.
2959
2960        :param lots: volume, integer count of lots >= 1.
2961        :param tp: float > 0, take profit price of stop-order.
2962        :param sl: float > 0, stop loss price of stop-order.
2963        :param expDate: it's a local date in the future.
2964                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2965        :return: JSON with response from broker server.
2966        """
2967        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2968
2969    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2970        """
2971        Close position of given instruments.
2972
2973        :param tickers: tickers list of instruments that must be closed.
2974        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2975                         This avoids unnecessary downloading data from the server.
2976        """
2977        if not tickers:
2978            uLogger.info("Tickers list is empty, nothing to close.")
2979
2980        else:
2981            if portfolio is None or not portfolio:
2982                portfolio = self.Overview(show=False)
2983
2984            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2985            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
2986
2987            for ticker in tickers:
2988                if ticker not in allOpenedTickers:
2989                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
2990                    continue
2991
2992                # search open trade info about instrument by ticker:
2993                instrument = {}
2994                for iType in TKS_INSTRUMENTS:
2995                    if instrument:
2996                        break
2997
2998                    for item in portfolio["stat"][iType]:
2999                        if item["ticker"] == ticker:
3000                            instrument = item
3001                            break
3002
3003                if instrument:
3004                    self.ticker = ticker
3005                    self.figi = instrument["figi"]
3006
3007                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3008                        self.ticker,
3009                        self.figi,
3010                        int(instrument["volume"]),
3011                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3012                    ))
3013
3014                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3015
3016                    if tradeLots > 0:
3017                        if instrument["blocked"] > 0:
3018                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3019                                instrument["blocked"],
3020                                self.ticker,
3021                                tradeLots,
3022                            ))
3023
3024                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3025                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3026
3027                    else:
3028                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
3029
3030    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3031        """
3032        Close all positions of given instruments with defined type.
3033
3034        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3035        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3036                         This avoids unnecessary downloading data from the server.
3037        """
3038        if iType not in TKS_INSTRUMENTS:
3039            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3040
3041        else:
3042            if portfolio is None or not portfolio:
3043                portfolio = self.Overview(show=False)
3044
3045            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3046            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3047
3048            if tickers and portfolio:
3049                self.CloseTrades(tickers, portfolio)
3050
3051            else:
3052                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3053
3054    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3055        """
3056        Universal method to create market or limit orders with all available parameters for current `accountId`.
3057        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3058
3059        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3060        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3061
3062        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3063        then broker immediately open market order as you can do simple --buy or --sell operations!
3064
3065        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3066        When current price will go up or down to target price value then broker opens a limit order.
3067        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3068
3069        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3070
3071        :param operation: string "Buy" or "Sell".
3072        :param orderType: string "Limit" or "Stop".
3073        :param lots: volume, integer count of lots >= 1.
3074        :param targetPrice: target price > 0. This is open trade price for limit order.
3075        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3076                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3077        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3078                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3079                         Stop loss order always executed by market price.
3080        :param expDate: string "Undefined" by default or local date in future.
3081                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3082                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3083                        A limit order has no expiration date, it lasts until the end of the trading day.
3084        :return: JSON with response from broker server.
3085        """
3086        if self.accountId is None or not self.accountId:
3087            uLogger.error("Variable `accountId` must be defined for using this method!")
3088            raise Exception("Account ID required")
3089
3090        if operation is None or not operation or operation not in ("Buy", "Sell"):
3091            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3092            raise Exception("Incorrect value")
3093
3094        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3095            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3096            raise Exception("Incorrect value")
3097
3098        if lots is None or lots < 1:
3099            uLogger.error("You must define trade volume > 0: integer count of lots!")
3100            raise Exception("Incorrect value")
3101
3102        if targetPrice is None or targetPrice <= 0:
3103            uLogger.error("Target price for limit-order must be greater than 0!")
3104            raise Exception("Incorrect value")
3105
3106        if limitPrice is None or limitPrice <= 0:
3107            limitPrice = targetPrice
3108
3109        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3110            stopType = "Limit"
3111
3112        if expDate is None or not expDate:
3113            expDate = "Undefined"
3114
3115        if not (self.ticker or self.figi):
3116            uLogger.error("Tocker or FIGI must be defined!")
3117            raise Exception("Ticker or FIGI required")
3118
3119        response = {}
3120        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3121        self.ticker = instrument["ticker"]
3122        self.figi = instrument["figi"]
3123
3124        if orderType == "Limit":
3125            uLogger.debug(
3126                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3127                    self.ticker, self.figi,
3128                    operation, lots, targetPrice, instrument["currency"],
3129                ))
3130
3131            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3132            self.body = str({
3133                "figi": self.figi,
3134                "quantity": str(lots),
3135                "price": FloatToNano(targetPrice),
3136                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3137                "accountId": str(self.accountId),
3138                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3139            })
3140            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3141
3142            if "orderId" in response.keys():
3143                uLogger.info(
3144                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3145                        response["orderId"],
3146                        self.ticker, self.figi,
3147                        operation, lots, targetPrice, instrument["currency"],
3148                    ))
3149
3150                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3151                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3152                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3153                            targetPrice, instrument["currency"],
3154                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3155                        ))
3156
3157                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3158                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3159                            targetPrice, instrument["currency"],
3160                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3161                        ))
3162
3163            else:
3164                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3165
3166        if orderType == "Stop":
3167            uLogger.debug(
3168                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3169                    self.ticker, self.figi,
3170                    operation, lots,
3171                    targetPrice, instrument["currency"],
3172                    limitPrice, instrument["currency"],
3173                    stopType, expDate,
3174                ))
3175
3176            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3177            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3178            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3179
3180            body = {
3181                "figi": self.figi,
3182                "quantity": str(lots),
3183                "price": FloatToNano(limitPrice),
3184                "stopPrice": FloatToNano(targetPrice),
3185                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3186                "accountId": str(self.accountId),
3187                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3188                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3189            }
3190
3191            if expDateUTC:
3192                body["expireDate"] = expDateUTC
3193
3194            self.body = str(body)
3195            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3196
3197            if "stopOrderId" in response.keys():
3198                uLogger.info(
3199                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3200                        response["stopOrderId"],
3201                        self.ticker, self.figi,
3202                        operation, lots,
3203                        targetPrice, instrument["currency"],
3204                        limitPrice, instrument["currency"],
3205                        TKS_STOP_ORDER_TYPES[stopOrderType],
3206                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3207                    ))
3208
3209                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3210                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3211                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3212                            targetPrice, instrument["currency"],
3213                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3214                        ))
3215
3216                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3217                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3218                            targetPrice, instrument["currency"],
3219                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3220                        ))
3221
3222            else:
3223                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3224
3225        return response
3226
3227    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3228        """
3229        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3230        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3231        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3232        See also: `Order()` docstring.
3233
3234        :param lots: volume, integer count of lots >= 1.
3235        :param targetPrice: target price > 0. This is open trade price for limit order.
3236        :return: JSON with response from broker server.
3237        """
3238        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3239
3240    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3241        """
3242        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3243        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3244        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3245        target price value then broker opens a limit order. See also: `Order()` docstring.
3246
3247        :param lots: volume, integer count of lots >= 1.
3248        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3249        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3250                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3251        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3252                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3253        :param expDate: string "Undefined" by default or local date in future.
3254                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3255                        This date is converting to UTC format for server.
3256        :return: JSON with response from broker server.
3257        """
3258        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3259
3260    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3261        """
3262        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3263        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3264        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3265        See also: `Order()` docstring.
3266
3267        :param lots: volume, integer count of lots >= 1.
3268        :param targetPrice: target price > 0. This is open trade price for limit order.
3269        :return: JSON with response from broker server.
3270        """
3271        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3272
3273    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3274        """
3275        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3276        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3277        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3278        target price value then broker opens a limit order. See also: `Order()` docstring.
3279
3280        :param lots: volume, integer count of lots >= 1.
3281        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3282        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3283                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3284        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3285                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3286        :param expDate: string "Undefined" by default or local date in future.
3287                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3288                        This date is converting to UTC format for server.
3289        :return: JSON with response from broker server.
3290        """
3291        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3292
3293    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3294        """
3295        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3296
3297        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3298        :param allOrdersIDs: pre-received lists of all active pending orders.
3299                             This avoids unnecessary downloading data from the server.
3300        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3301        """
3302        if self.accountId is None or not self.accountId:
3303            uLogger.error("Variable `accountId` must be defined for using this method!")
3304            raise Exception("Account ID required")
3305
3306        if orderIDs:
3307            if allOrdersIDs is None or not allOrdersIDs:
3308                rawOrders = self.RequestPendingOrders()
3309                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3310
3311            if allStopOrdersIDs is None or not allStopOrdersIDs:
3312                rawStopOrders = self.RequestStopOrders()
3313                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3314
3315            for orderID in orderIDs:
3316                idInPendingOrders = orderID in allOrdersIDs
3317                idInStopOrders = orderID in allStopOrdersIDs
3318
3319                if not (idInPendingOrders or idInStopOrders):
3320                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3321                    continue
3322
3323                else:
3324                    if idInPendingOrders:
3325                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3326
3327                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3328                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3329                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3330                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3331
3332                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3333                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3334                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3335
3336                        else:
3337                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3338
3339                    elif idInStopOrders:
3340                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3341
3342                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3343                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3344                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3345                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3346
3347                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3348                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3349                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3350
3351                        else:
3352                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3353
3354                    else:
3355                        continue
3356
3357    def CloseAllOrders(self) -> None:
3358        """
3359        Gets a list of open pending and stop orders and cancel it all.
3360        """
3361        rawOrders = self.RequestPendingOrders()
3362        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3363        lenOrders = len(allOrdersIDs)
3364
3365        rawStopOrders = self.RequestStopOrders()
3366        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3367        lenSOrders = len(allStopOrdersIDs)
3368
3369        if lenOrders > 0 or lenSOrders > 0:
3370            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3371
3372            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3373
3374        else:
3375            uLogger.info("Orders not found, nothing to cancel.")
3376
3377    def CloseAll(self, *args) -> None:
3378        """
3379        Close all available (not blocked) opened trades and orders.
3380
3381        Also, you can select one or more keywords case-insensitive:
3382        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3383
3384        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3385        """
3386        overview = self.Overview(show=False)  # get all open trades info
3387
3388        if len(args) == 0:
3389            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3390            self.CloseAllOrders()  # close all pending and stop orders
3391
3392            for iType in TKS_INSTRUMENTS:
3393                if iType != "Currencies":
3394                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3395
3396        else:
3397            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3398            lowerArgs = [x.lower() for x in args]
3399
3400            if "orders" in lowerArgs:
3401                self.CloseAllOrders()  # close all pending and stop orders
3402
3403            for iType in TKS_INSTRUMENTS:
3404                if iType.lower() in lowerArgs and iType != "Currencies":
3405                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3406
3407    @staticmethod
3408    def ParseOrderParameters(operation, **inputParameters):
3409        """
3410        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3411
3412        :param operation: string "Buy" or "Sell".
3413        :param inputParameters: this is dict of strings that looks like this
3414               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3415               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3416               "prices" key: one or more prices to open limit-orders
3417               Counts of values in lots and prices lists must be equals!
3418        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3419        """
3420        # TODO: update order grid work with api v2
3421        pass
3422        # uLogger.debug("Input parameters: {}".format(inputParameters))
3423        #
3424        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3425        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3426        #     raise Exception("Incorrect value")
3427        #
3428        # if "l" in inputParameters.keys():
3429        #     inputParameters["lots"] = inputParameters.pop("l")
3430        #
3431        # if "p" in inputParameters.keys():
3432        #     inputParameters["prices"] = inputParameters.pop("p")
3433        #
3434        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3435        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3436        #     raise Exception("Incorrect value")
3437        #
3438        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3439        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3440        #
3441        # if len(lots) != len(prices):
3442        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3443        #     raise Exception("Incorrect value")
3444        #
3445        # uLogger.debug("Extracted parameters for orders:")
3446        # uLogger.debug("lots = {}".format(lots))
3447        # uLogger.debug("prices = {}".format(prices))
3448        #
3449        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3450        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3451        # uLogger.debug("Order parameters: {}".format(result))
3452        #
3453        # return result
3454
3455    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3456        """
3457        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3458
3459        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3460        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3461        """
3462        result = False
3463        msg = "Instrument not defined!"
3464
3465        if portfolio is None or not portfolio:
3466            portfolio = self.Overview(show=False)
3467
3468        if self.ticker:
3469            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3470            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3471
3472            for iType in TKS_INSTRUMENTS:
3473                for instrument in portfolio["stat"][iType]:
3474                    if instrument["ticker"] == self.ticker:
3475                        result = True
3476                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3477                        break
3478
3479        elif self.figi:
3480            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3481            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3482
3483            for iType in TKS_INSTRUMENTS:
3484                for instrument in portfolio["stat"][iType]:
3485                    if instrument["figi"] == self.figi:
3486                        result = True
3487                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3488                        break
3489
3490        else:
3491            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3492
3493        uLogger.debug(msg)
3494
3495        return result
3496
3497    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3498        """
3499        Returns instrument is in the user's portfolio if it presents there.
3500        Instrument must be defined by `ticker` (highly priority) or `figi`.
3501
3502        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3503        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3504        """
3505        result = None
3506        msg = "Instrument not defined!"
3507
3508        if portfolio is None or not portfolio:
3509            portfolio = self.Overview(show=False)
3510
3511        if self.ticker:
3512            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3513            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3514
3515            for iType in TKS_INSTRUMENTS:
3516                for instrument in portfolio["stat"][iType]:
3517                    if instrument["ticker"] == self.ticker:
3518                        result = instrument
3519                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3520                        break
3521
3522        elif self.figi:
3523            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3524            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3525
3526            for iType in TKS_INSTRUMENTS:
3527                for instrument in portfolio["stat"][iType]:
3528                    if instrument["figi"] == self.figi:
3529                        result = instrument
3530                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3531                        break
3532
3533        else:
3534            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3535
3536        uLogger.debug(msg)
3537
3538        return result
3539
3540    def RequestLimits(self) -> dict:
3541        """
3542        Method for obtaining the available funds for withdrawal for current `accountId`.
3543
3544        See also:
3545        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3546        - `OverviewLimits()` method
3547
3548        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3549                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3550                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3551                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3552        """
3553        if self.accountId is None or not self.accountId:
3554            uLogger.error("Variable `accountId` must be defined for using this method!")
3555            raise Exception("Account ID required")
3556
3557        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3558
3559        self.body = str({"accountId": self.accountId})
3560        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3561        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3562
3563        uLogger.debug("Records about available funds for withdrawal successfully received")
3564
3565        return rawLimits
3566
3567    def OverviewLimits(self, show: bool = False) -> dict:
3568        """
3569        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3570
3571        See also: `RequestLimits()`.
3572
3573        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3574        :return: dict with raw parsed data from server and some calculated statistics about it.
3575        """
3576        if self.accountId is None or not self.accountId:
3577            uLogger.error("Variable `accountId` must be defined for using this method!")
3578            raise Exception("Account ID required")
3579
3580        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3581
3582        view = {
3583            "rawLimits": rawLimits,
3584            "limits": {  # parsed data for every currency:
3585                "money": {  # this is an array of portfolio currency positions
3586                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3587                },
3588                "blocked": {  # this is an array of blocked currency
3589                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3590                },
3591                "blockedGuarantee": {  # this is locked money under collateral for futures
3592                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3593                },
3594            },
3595        }
3596
3597        # --- Prepare text table with limits in human-readable format:
3598        if show:
3599            info = [
3600                "# Withdrawal limits\n\n",
3601                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3602                "* **Account ID:** [{}]\n".format(self.accountId),
3603                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3604                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3605            ]
3606
3607            for curr in view["limits"]["money"].keys():
3608                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3609                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3610                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3611
3612                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3613                    "[{}]".format(curr),
3614                    "{:.2f}".format(view["limits"]["money"][curr]),
3615                    "{:.2f}".format(availableMoney),
3616                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3617                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3618                )
3619
3620                if curr == "rub":
3621                    info.insert(5, infoStr)  # insert at first position in table and after headers
3622
3623                else:
3624                    info.append(infoStr)
3625
3626            infoText = "".join(info)
3627
3628            uLogger.info(infoText)
3629
3630            if self.withdrawalLimitsFile:
3631                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3632                    fH.write(infoText)
3633
3634                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3635
3636        return view
3637
3638    def RequestAccounts(self) -> dict:
3639        """
3640        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3641
3642        See also:
3643        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3644        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3645        - `OverviewUserInfo()` method
3646
3647        :return: dict with raw data from server that contains accounts info. Example of dict:
3648                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3649                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3650                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3651                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3652        """
3653        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3654
3655        self.body = str({})
3656        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3657        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3658
3659        uLogger.debug("Records about available accounts successfully received")
3660
3661        return rawAccounts
3662
3663    def RequestUserInfo(self) -> dict:
3664        """
3665        Method for requesting common user's information.
3666
3667        See also:
3668        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3669        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3670        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3671        - `OverviewUserInfo()` method
3672
3673        :return: dict with raw data from server that contains user's information. Example of dict:
3674                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3675                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3676        """
3677        uLogger.debug("Requesting common user's information. Wait, please...")
3678
3679        self.body = str({})
3680        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3681        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3682
3683        uLogger.debug("Records about current user successfully received")
3684
3685        return rawUserInfo
3686
3687    def RequestMarginStatus(self, accountId: str = None) -> dict:
3688        """
3689        Method for requesting margin calculation for defined account ID.
3690
3691        See also:
3692        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3693        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3694        - `OverviewUserInfo()` method
3695
3696        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3697        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3698                 Example of responses:
3699                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3700                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3701                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3702                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3703                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3704                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3705        """
3706        if accountId is None or not accountId:
3707            if self.accountId is None or not self.accountId:
3708                uLogger.error("Variable `accountId` must be defined for using this method!")
3709                raise Exception("Account ID required")
3710
3711            else:
3712                accountId = self.accountId  # use `self.accountId` (main ID) by default
3713
3714        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3715
3716        self.body = str({"accountId": accountId})
3717        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3718        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3719
3720        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3721            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3722            rawMargin = {}
3723
3724        else:
3725            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3726
3727        return rawMargin
3728
3729    def RequestTariffLimits(self) -> dict:
3730        """
3731        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3732
3733        See also:
3734        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3735        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3736        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3737        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3738        - `OverviewUserInfo()` method
3739
3740        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3741                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3742                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3743        """
3744        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3745
3746        self.body = str({})
3747        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3748        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3749
3750        uLogger.debug("Records with limits of current tariff successfully received")
3751
3752        return rawTariffLimits
3753
3754    def RequestBondCoupons(self, iJSON: dict) -> dict:
3755        """
3756        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3757        then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
3758        All dates are in UTC timezone.
3759
3760        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3761        Documentation:
3762        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3763        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3764
3765        See also: `ExtendBondsData()`.
3766
3767        :param iJSON: raw json data of a bond from broker server, example: `iJSON = self.iList["Bonds"][self.ticker]`
3768                      If raw iJSON is not data of bond then server returns an error [400] with message:
3769                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3770        :return: dictionary with bond payment calendar. Response example:
3771                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3772                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3773                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3774                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3775        """
3776        if iJSON["figi"] is None or not iJSON["figi"]:
3777            uLogger.error("FIGI must be defined for using this method!")
3778            raise Exception("FIGI required")
3779
3780        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3781        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3782
3783        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3784            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3785            self.figi,
3786            startDate,
3787            endDate,
3788        ))
3789
3790        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3791        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3792        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3793
3794        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3795            uLogger.warning("Instrument type is not bond!")
3796
3797        else:
3798            uLogger.debug("Records about bond payment calendar successfully received")
3799
3800        return calendar
3801
3802    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3803        """
3804        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3805        pandas dataframe with more information about bonds: main info, current prices, bond payment calendar,
3806        coupon yields, current yields and some statistics etc.
3807
3808        WARNING! This is too long operation if a lot of bonds requested from broker server.
3809
3810        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3811
3812        :param instruments: list of strings with tickers or FIGIs.
3813        :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default: `ext-bonds.xlsx`,
3814                     for further used by data scientists or stock analytics.
3815        :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker.
3816                 In XLSX-file and pandas dataframe fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3817        """
3818        if instruments is None or not instruments:
3819            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3820            raise Exception("Ticker or FIGI required")
3821
3822        if isinstance(instruments, str):
3823            instruments = [instruments]
3824
3825        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3826
3827        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3828
3829        iCount = len(uniqueInstruments)
3830        tooLong = iCount >= 20
3831        if tooLong:
3832            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3833
3834        bonds = None
3835        for i, self.figi in enumerate(uniqueInstruments):
3836            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3837
3838            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3839                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3840                rawBond = self.SearchByFIGI(requestPrice=True)
3841
3842                # Widen raw data with UTC current time (iData["actualDateTime"]):
3843                actualDate = datetime.now(tzutc())
3844                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3845
3846                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3847                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3848
3849                # Replace some values with human-readable:
3850                iData["nominalCurrency"] = iData["nominal"]["currency"]
3851                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3852                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3853                iData["aciCurrency"] = iData["aciValue"]["currency"]
3854                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3855                iData["issueSize"] = int(iData["issueSize"])
3856                iData["issueSizePlan"] = int(iData["issueSize"])
3857                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3858                iData["minPriceIncrement"] = NanoToFloat(iData["minPriceIncrement"]["units"], iData["minPriceIncrement"]["nano"]) if "minPriceIncrement" in iData.keys() else 0.
3859                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3860
3861                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3862                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3863                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3864                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3865                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3866                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3867                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3868                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3869                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3870                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3871                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3872
3873                # Widen raw data with calendar data from `rawCalendar` values:
3874                calendarData = []
3875                for item in iData["rawCalendar"]["events"]:
3876                    calendarData.append({
3877                        "couponDate": item["couponDate"],
3878                        "couponNumber": int(item["couponNumber"]),
3879                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3880                        "payCurrency": item["payOneBond"]["currency"],
3881                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3882                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3883                        "couponStartDate": item["couponStartDate"],
3884                        "couponEndDate": item["couponEndDate"],
3885                        "couponPeriod": item["couponPeriod"],
3886                    })
3887
3888                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3889                if "maturityDate" not in iData.keys():
3890                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3891
3892                # Widen raw data with Coupon Rate.
3893                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3894                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3895                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3896                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3897
3898                # Widen raw data with Yield to Maturity (YTM) on current date.
3899                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3900                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3901                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3902                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3903                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3904                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3905
3906                iData["calendar"] = calendarData  # adds calendar at the end
3907
3908                # Remove not used data:
3909                iData.pop("uid")
3910                iData.pop("positionUid")
3911                iData.pop("currentPrice")
3912                iData.pop("rawCalendar")
3913
3914                colNames = list(iData.keys())
3915                if bonds is None:
3916                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3917
3918                else:
3919                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3920
3921            else:
3922                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3923
3924            processed = round(100 * (i + 1) / iCount, 1)
3925            if tooLong and processed % 5 == 0:
3926                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3927
3928            else:
3929                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3930
3931        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3932
3933        # Saving bonds from pandas dataframe to XLSX sheet:
3934        if xlsx and self.bondsXLSXFile:
3935            with pd.ExcelWriter(
3936                    path=self.bondsXLSXFile,
3937                    date_format=TKS_DATE_FORMAT,
3938                    datetime_format=TKS_DATE_TIME_FORMAT,
3939                    mode="w",
3940            ) as writer:
3941                bonds.to_excel(
3942                    writer,
3943                    sheet_name="Extended bonds data",
3944                    index=True,
3945                    encoding="UTF-8",
3946                    freeze_panes=(1, 1),
3947                )  # saving as XLSX-file with freeze first row and column as headers
3948
3949            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3950
3951        return bonds
3952
3953    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3954        """
3955        Creates bond payments calendar as pandas dataframe, and you can also save it to the XLSX-file.
3956
3957        WARNING! This is too long operation if a lot of bonds requested from broker server.
3958
3959        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3960
3961        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
3962                        extended information about bonds: main info, current prices, bond payment calendar,
3963                        coupon yields, current yields and some statistics etc.
3964                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3965        :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, default: `calendar.xlsx`,
3966                     for further used by data scientists or stock analytics.
3967        :return: pandas dataframe with only bond payments calendar data.
3968        """
3969        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3970            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3971
3972        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3973
3974        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3975        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3976        calendar = None
3977        for bond in extBonds.iterrows():
3978            for item in bond[1]["calendar"]:
3979                cData = {
3980                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3981                    "couponDate": item["couponDate"],
3982                    "figi": bond[1]["figi"],
3983                    "ticker": bond[1]["ticker"],
3984                    "name": bond[1]["name"],
3985                    "couponNumber": item["couponNumber"],
3986                    "payOneBond": item["payOneBond"],
3987                    "payCurrency": item["payCurrency"],
3988                    "couponType": item["couponType"],
3989                    "couponPeriod": item["couponPeriod"],
3990                    "fixDate": item["fixDate"],
3991                    "couponStartDate": item["couponStartDate"],
3992                    "couponEndDate": item["couponEndDate"],
3993                }
3994
3995                if calendar is None:
3996                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
3997
3998                else:
3999                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4000
4001        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4002
4003        # Saving calendar from pandas dataframe to XLSX sheet:
4004        if xlsx:
4005            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4006
4007            with pd.ExcelWriter(
4008                    path=xlsxCalendarFile,
4009                    date_format=TKS_DATE_FORMAT,
4010                    datetime_format=TKS_DATE_TIME_FORMAT,
4011                    mode="w",
4012            ) as writer:
4013                humanReadable = calendar.copy(deep=True)
4014                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4015                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4016                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4017                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4018                humanReadable.columns = colNames  # human-readable column names
4019
4020                humanReadable.to_excel(
4021                    writer,
4022                    sheet_name="Bond payments calendar",
4023                    index=False,
4024                    encoding="UTF-8",
4025                    freeze_panes=(1, 2),
4026                )  # saving as XLSX-file with freeze first row and column as headers
4027
4028                del humanReadable  # release df in memory
4029
4030            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4031
4032        return calendar
4033
4034    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4035        """
4036        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4037
4038        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
4039
4040        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
4041                        extended information about bonds: main info, current prices, bond payment calendar,
4042                        coupon yields, current yields and some statistics etc.
4043                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4044        :param show: if `True` then also printing bonds payment calendar to the console,
4045                     otherwise save to file `calendarFile` only. `False` by default.
4046        :return: multilines text in Markdown format with bonds payment calendar as a table.
4047        """
4048        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4049            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4050
4051        infoText = "# Bond payments calendar\n\n"
4052
4053        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate pandas dataframe with full calendar data
4054
4055        if not calendar.empty:
4056            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4057
4058            info = [
4059                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4060                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4061            ]
4062
4063            newMonth = False
4064            notOneBond = calendar["figi"].nunique() > 1
4065            for i, bond in enumerate(calendar.iterrows()):
4066                if newMonth and notOneBond:
4067                    info.append(splitLine)
4068
4069                info.append(
4070                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4071                        "  +" if bond[1]["paid"] else "  —",
4072                        bond[1]["couponDate"].split("T")[0],
4073                        bond[1]["figi"],
4074                        bond[1]["ticker"],
4075                        bond[1]["couponNumber"],
4076                        "{} {}".format(
4077                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4078                            bond[1]["payCurrency"],
4079                        ),
4080                        bond[1]["couponType"],
4081                        bond[1]["couponPeriod"],
4082                        bond[1]["fixDate"].split("T")[0],
4083                    )
4084                )
4085
4086                if i < len(calendar.values) - 1:
4087                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4088                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4089                    newMonth = False if curDate.month == nextDate.month else True
4090
4091                else:
4092                    newMonth = False
4093
4094            infoText += "".join(info)
4095
4096            if show:
4097                uLogger.info("{}".format(infoText))
4098
4099            if self.calendarFile is not None:
4100                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4101                    fH.write(infoText)
4102
4103                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4104
4105        else:
4106            infoText += "No data\n"
4107
4108        return infoText
4109
4110    def OverviewAccounts(self, show: bool = False) -> dict:
4111        """
4112        Method for parsing and show simple table with all available user accounts.
4113
4114        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4115
4116        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4117        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4118                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4119                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4120                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4121                                                        "closed": "—", "access": "Full access" }, ...}}`
4122        """
4123        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4124
4125        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4126        accounts = {
4127            item["id"]: {
4128                "type": TKS_ACCOUNT_TYPES[item["type"]],
4129                "name": item["name"],
4130                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4131                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4132                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4133                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4134            } for item in rawAccounts["accounts"]
4135        }
4136
4137        # Raw and parsed data with some fields replaced in "stat" section:
4138        view = {
4139            "rawAccounts": rawAccounts,
4140            "stat": accounts,
4141        }
4142
4143        # --- Prepare simple text table with only accounts data in human-readable format:
4144        if show:
4145            info = [
4146                "# User accounts\n\n",
4147                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4148                "| Account ID   | Type                      | Status                    | Name                           |\n",
4149                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4150            ]
4151
4152            for account in view["stat"].keys():
4153                info.extend([
4154                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4155                        account,
4156                        view["stat"][account]["type"],
4157                        view["stat"][account]["status"],
4158                        view["stat"][account]["name"],
4159                    )
4160                ])
4161
4162            infoText = "".join(info)
4163
4164            uLogger.info(infoText)
4165
4166            if self.userAccountsFile:
4167                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4168                    fH.write(infoText)
4169
4170                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4171
4172        return view
4173
4174    def OverviewUserInfo(self, show: bool = False) -> dict:
4175        """
4176        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4177
4178        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4179
4180        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4181        :return: dict with raw parsed data from server and some calculated statistics about it.
4182        """
4183        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4184        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4185        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4186        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4187        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4188        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4189
4190        # This is dict with parsed common user data:
4191        userInfo = {
4192            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4193            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4194            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4195            "tariff": rawUserInfo["tariff"],
4196        }
4197
4198        # This is an array of dict with parsed margin statuses for every account IDs:
4199        margins = {}
4200        for accountId in accounts.keys():
4201            if rawMargins[accountId]:
4202                margins[accountId] = {
4203                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4204                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4205                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4206                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4207                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4208                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4209                }
4210
4211            else:
4212                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4213
4214        unary = {}  # unary-connection limits
4215        for item in rawTariffLimits["unaryLimits"]:
4216            if item["limitPerMinute"] in unary.keys():
4217                unary[item["limitPerMinute"]].extend(item["methods"])
4218
4219            else:
4220                unary[item["limitPerMinute"]] = item["methods"]
4221
4222        stream = {}  # stream-connection limits
4223        for item in rawTariffLimits["streamLimits"]:
4224            if item["limit"] in stream.keys():
4225                stream[item["limit"]].extend(item["streams"])
4226
4227            else:
4228                stream[item["limit"]] = item["streams"]
4229
4230        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4231        limits = {
4232            "unary": unary,
4233            "stream": stream,
4234        }
4235
4236        # Raw and parsed data as an output result:
4237        view = {
4238            "rawUserInfo": rawUserInfo,
4239            "rawAccounts": rawAccounts,
4240            "rawMargins": rawMargins,
4241            "rawTariffLimits": rawTariffLimits,
4242            "stat": {
4243                "userInfo": userInfo,
4244                "accounts": accounts,
4245                "margins": margins,
4246                "limits": limits,
4247            },
4248        }
4249
4250        # --- Prepare text table with user information in human-readable format:
4251        if show:
4252            info = [
4253                "# Full user information\n\n",
4254                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4255                "## Common information\n\n",
4256                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4257                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4258                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4259                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4260                "\n## User accounts\n\n",
4261            ]
4262
4263            for account in view["stat"]["accounts"].keys():
4264                info.extend([
4265                    "### ID: [{}]\n\n".format(account),
4266                    "| Parameters           | Values                                                       |\n",
4267                    "|----------------------|--------------------------------------------------------------|\n",
4268                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4269                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4270                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4271                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4272                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4273                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4274                ])
4275
4276                if margins[account]:
4277                    info.extend([
4278                        "| Margin status:       | Enabled                                                      |\n",
4279                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4280                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4281                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4282                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4283                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4284                    ])
4285
4286                else:
4287                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4288
4289            info.extend([
4290                "\n## Current user tariff limits\n",
4291                "\nSee also:\n",
4292                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4293                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4294                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4295                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4296                "\n### Unary limits\n",
4297            ])
4298
4299            if unary:
4300                for key, values in sorted(unary.items()):
4301                    info.append("\n* Max requests per minute: {}\n".format(key))
4302
4303                    for value in values:
4304                        info.append("  - {}\n".format(value))
4305
4306            else:
4307                info.append("\nNot available\n")
4308
4309            info.append("\n### Stream limits\n")
4310
4311            if stream:
4312                for key, values in sorted(stream.items()):
4313                    info.append("\n* Max stream connections: {}\n".format(key))
4314
4315                    for value in values:
4316                        info.append("  - {}\n".format(value))
4317
4318            else:
4319                info.append("\nNot available\n")
4320
4321            infoText = "".join(info)
4322
4323            uLogger.info(infoText)
4324
4325            if self.userInfoFile:
4326                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4327                    fH.write(infoText)
4328
4329                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4330
4331        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
196    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
197        """
198        Main class init.
199
200        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
201        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
202                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
203        :param useCache: use default cache file with raw data to use instead of `iList`.
204                         True by default. Cache is auto-update if new day has come.
205                         If you don't want to use cache and always updates raw data then set `useCache=False`.
206        :param defaultCache: path to default cache file. `dump.json` by default.
207        """
208        if token is None or not token:
209            try:
210                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
211                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
212
213            except KeyError:
214                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
215                raise Exception("Token required")
216
217        else:
218            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
219            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
220
221        if accountId is None or not accountId:
222            try:
223                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
224                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
225
226            except KeyError:
227                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
228
229        else:
230            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
231            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
232
233        self.version = __version__  # duplicate here used TKSBrokerAPI main version
234        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
235
236        Latest version: https://pypi.org/project/tksbrokerapi/
237        """
238
239        self.aliases = TKS_TICKER_ALIASES
240        """Some aliases instead official tickers.
241
242        See also: `TKSEnums.TKS_TICKER_ALIASES`
243        """
244
245        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
246
247        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
248
249        self.ticker = ""
250        """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
251
252        See also: `SearchByTicker()`, `SearchInstruments()`.
253        """
254
255        self.figi = ""
256        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`.
257
258        See also: `SearchByFIGI()`, `SearchInstruments()`.
259        """
260
261        self.depth = 1
262        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
263
264        See also: `GetCurrentPrices()`.
265        """
266
267        self.server = r"https://invest-public-api.tinkoff.ru/rest"
268        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
269
270        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
271        """
272
273        uLogger.debug("Broker API server: {}".format(self.server))
274
275        self.timeout = 15
276        """Server operations timeout in seconds. Default: `15`.
277
278        See also: `SendAPIRequest()`.
279        """
280
281        self.headers = {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {}".format(self.token)}
282        """Headers which send in every request to broker server. Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
283
284        See also: `SendAPIRequest()`.
285        """
286
287        self.body = None
288        """Request body which send to broker server. Default: `None`.
289
290        See also: `SendAPIRequest()`.
291        """
292
293        self.historyFile = None
294        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only pandas dataframe.
295
296        See also: `History()`.
297        """
298
299        self.htmlHistoryFile = "index.html"
300        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
301
302        See also: `ShowHistoryChart()`.
303        """
304
305        self.instrumentsFile = "instruments.md"
306        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
307
308        See also: `ShowInstrumentsInfo()`.
309        """
310
311        self.searchResultsFile = "search-results.md"
312        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
313
314        See also: `SearchInstruments()`.
315        """
316
317        self.pricesFile = "prices.md"
318        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
319
320        See also: `GetListOfPrices()`.
321        """
322
323        self.infoFile = "info.md"
324        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
325
326        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
327        """
328
329        self.bondsXLSXFile = "ext-bonds.xlsx"
330        """Filename where wider pandas dataframe with more information about bonds: main info, current prices, 
331        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
332
333        See also: `ExtendBondsData()`.
334        """
335
336        self.calendarFile = "calendar.md"
337        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
338        
339        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
340
341        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
342        """
343
344        self.overviewFile = "overview.md"
345        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
346
347        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
348        """
349
350        self.overviewDigestFile = "overview-digest.md"
351        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
352
353        See also: `Overview()` with parameter `details="digest"`.
354        """
355
356        self.overviewPositionsFile = "overview-positions.md"
357        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
358
359        See also: `Overview()` with parameter `details="positions"`.
360        """
361
362        self.overviewOrdersFile = "overview-orders.md"
363        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
364
365        See also: `Overview()` with parameter `details="orders"`.
366        """
367
368        self.overviewAnalyticsFile = "overview-analytics.md"
369        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
370
371        See also: `Overview()` with parameter `details="analytics"`.
372        """
373
374        self.reportFile = "deals.md"
375        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
376
377        See also: `Deals()`.
378        """
379
380        self.withdrawalLimitsFile = "limits.md"
381        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
382
383        See also: `OverviewLimits()` and `RequestLimits()`.
384        """
385
386        self.userInfoFile = "user-info.md"
387        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
388
389        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
390        """
391
392        self.userAccountsFile = "accounts.md"
393        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
394
395        See also: `OverviewAccounts()`, `RequestAccounts()`.
396        """
397
398        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
399        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
400
401        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
402
403        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
404        """
405
406        self.iList = None  # init iList for raw instruments data
407        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
408        
409        See also: `Listing()`, `DumpInstruments()`.
410        """
411
412        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
413        if useCache:
414            if os.path.exists(self.iListDumpFile):
415                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
416                curTime = datetime.now(tzutc())
417
418                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
419                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
420
421                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
422
423                else:
424                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
425
426                    uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile)))
427                    uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
428
429            else:
430                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
431                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
432
433        else:
434            self.iList = self.Listing()  # request new raw instruments data from broker server
435            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
436
437        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
438        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
439
440        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
441        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only pandas dataframe.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
465    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict:
466        """
467        Send GET or POST request to broker server and receive JSON object.
468
469        self.header: must be defining with dictionary of headers.
470        self.body: if define then used as request body. None by default.
471        self.timeout: global request timeout, 15 seconds by default.
472        :param url: url with REST request.
473        :param reqType: send "GET" or "POST" request. "GET" by default.
474        :param retry: how many times retry after first request if an 5xx server errors occurred.
475        :param pause: sleep time in seconds between retries.
476        :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc.
477        :return: response JSON (dictionary) from broker.
478        """
479        if reqType not in ("GET", "POST"):
480            uLogger.error("You can define request type: 'GET' or 'POST'!")
481            raise Exception("Incorrect value")
482
483        if debug:
484            uLogger.debug("Request parameters:")
485            uLogger.debug("    - REST API URL: {}".format(url))
486            uLogger.debug("    - request type: {}".format(reqType))
487            uLogger.debug("    - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***")))
488            uLogger.debug("    - body: {}".format(self.body))
489
490        # fast hack to avoid all operations with some tickers/FIGI
491        responseJSON = {}
492        oK = True
493        for item in self.exclude:
494            if item in url:
495                if debug:
496                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
497
498                oK = False
499                break
500
501        if oK:
502            counter = 0
503            response = None
504            errMsg = ""
505
506            while not response and counter <= retry:
507                if reqType == "GET":
508                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
509
510                if reqType == "POST":
511                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
512
513                if debug:
514                    uLogger.debug("Response:")
515                    uLogger.debug("    - status code: {}".format(response.status_code))
516                    uLogger.debug("    - reason: {}".format(response.reason))
517                    uLogger.debug("    - body length: {}".format(len(response.text)))
518                    uLogger.debug("    - headers: {}".format(response.headers))
519
520                # Server returns some headers:
521                # - `x-ratelimit-limit` - shows the settings of the current user limit for this method.
522                # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute.
523                # - `x-ratelimit-reset` - time in seconds before resetting the request counter.
524                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
525                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
526                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
527                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
528                    sleep(rateLimitWait)
529
530                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
531                if 400 <= response.status_code < 500:
532                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
533                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
534                    counter = retry + 1
535
536                if 500 <= response.status_code < 600:
537                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
538                    uLogger.debug("    - not oK, {}".format(errMsg))
539                    counter += 1
540
541                    if counter <= retry:
542                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
543                        sleep(pause)
544
545            responseJSON = self._ParseJSON(response.text)
546
547            if errMsg:
548                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
549                uLogger.error("    - not oK, {}".format(errMsg))
550
551        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
  • debug: if True then print more debug information, e.g. request and response parameters, headers etc.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
584    def Listing(self) -> dict:
585        """
586        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
587
588        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
589        """
590        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
591        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
592
593        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
594        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
595        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
596
597        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
598        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
599        poolUpdater.close()
600
601        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
602        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
603        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
604
605        # calculate minimum price increment (step) for all instruments and set up instrument's type:
606        for iType in iList.keys():
607            for ticker in iList[iType]:
608                iList[iType][ticker]["type"] = iType
609
610                if "minPriceIncrement" in iList[iType][ticker].keys():
611                    iList[iType][ticker]["step"] = NanoToFloat(
612                        iList[iType][ticker]["minPriceIncrement"]["units"],
613                        iList[iType][ticker]["minPriceIncrement"]["nano"],
614                    )
615
616                else:
617                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
618
619        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
621    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
622        """
623        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
624
625        See also: `DumpInstruments()`, `Listing()`.
626
627        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
628                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
629        """
630        if self.iListDumpFile is None or not self.iListDumpFile:
631            uLogger.error("Output name of dump file must be defined!")
632            raise Exception("Filename required")
633
634        if not self.iList or forceUpdate:
635            self.iList = self.Listing()
636
637        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
638
639        # Save as XLSX with separated sheets for every type of instruments:
640        with pd.ExcelWriter(
641                path=xlsxDumpFile,
642                date_format=TKS_DATE_FORMAT,
643                datetime_format=TKS_DATE_TIME_FORMAT,
644                mode="w",
645        ) as writer:
646            for iType in TKS_INSTRUMENTS:
647                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
648                df = df[sorted(df)]  # sorted by column names
649                df = df.applymap(
650                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
651                    na_action="ignore",
652                )  # converting numbers from nano-type to float in every cell
653                df.to_excel(
654                    writer,
655                    sheet_name=iType,
656                    encoding="UTF-8",
657                    freeze_panes=(1, 1),
658                )  # saving as XLSX-file with freeze first row and column as headers
659
660        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
662    def DumpInstruments(self, forceUpdate: bool = True) -> str:
663        """
664        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
665        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
666
667        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
668
669        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
670                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
671        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
672        """
673        if self.iListDumpFile is None or not self.iListDumpFile:
674            uLogger.error("Output name of dump file must be defined!")
675            raise Exception("Filename required")
676
677        if not self.iList or forceUpdate:
678            self.iList = self.Listing()
679
680        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
681        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
682            fH.write(jsonDump)
683
684        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
685
686        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
688    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
689        """
690        Show information about one instrument defined by json data and prints it in Markdown format.
691
692        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
693
694        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
695        :param show: if `True` then also printing information about instrument and its current price.
696        :return: multilines text in Markdown format with information about one instrument.
697        """
698        splitLine = "|                                                             |                                                        |\n"
699        infoText = ""
700
701        if iJSON is not None and iJSON and isinstance(iJSON, dict):
702            info = [
703                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
704                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
705                "| Parameters                                                  | Values                                                 |\n",
706                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
707                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
708                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
709            ]
710
711            if "sector" in iJSON.keys() and iJSON["sector"]:
712                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
713
714            info.append("| Country of instrument:                                      | {:<54} |\n".format("{}{}".format(
715                "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "",
716                iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "",
717            )))
718
719            info.extend([
720                splitLine,
721                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
722                "| Exchange:                                                   | {:<54} |\n".format(iJSON["exchange"]),
723            ])
724
725            if "isin" in iJSON.keys() and iJSON["isin"]:
726                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
727
728            if "classCode" in iJSON.keys():
729                info.append("| Class Code:                                                 | {:<54} |\n".format(iJSON["classCode"]))
730
731            info.extend([
732                splitLine,
733                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
734                splitLine,
735                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
736                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
737                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
738            ])
739
740            if iJSON["figi"]:
741                self.figi = iJSON["figi"]
742                iJSON = iJSON | self.RequestTradingStatus()
743
744                info.extend([
745                    splitLine,
746                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
747                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
748                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
749                ])
750
751            info.append(splitLine)
752
753            if "type" in iJSON.keys() and iJSON["type"]:
754                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
755
756            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
757                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
758
759            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
760                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
761
762            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
763                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
764
765            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
766                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
767
768            if "focusType" in iJSON.keys() and iJSON["focusType"]:
769                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
770
771            if "assetType" in iJSON.keys() and iJSON["assetType"]:
772                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
773
774            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
775                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
776
777            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
778                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
779
780            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
781                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
782
783            if "currency" in iJSON.keys():
784                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
785
786            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
787                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
788
789            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
790                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
791
792            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
793                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
794
795            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
796                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
797
798            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
799                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
800
801            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
802                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
803
804            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
805                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
806
807            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
808                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
809
810            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
811                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
812
813            iExt = None
814            if iJSON["type"] == "Bonds":
815                info.extend([
816                    splitLine,
817                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
818                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
819                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
820                        iJSON["nominal"]["currency"],
821                    )),
822                ])
823
824                if "floatingCouponFlag" in iJSON.keys():
825                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
826
827                if "amortizationFlag" in iJSON.keys():
828                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
829
830                info.append(splitLine)
831
832                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
833                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
834
835                iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
836
837                info.extend([
838                    "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
839                    "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
840                    "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
841                ])
842
843                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
844                    info.append("| Current Accrued Interest (ACI):                             | {:<54} |\n".format("{:.2f} {}".format(
845                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
846                        iJSON["aciValue"]["currency"]
847                    )))
848
849            if "currentPrice" in iJSON.keys():
850                info.append(splitLine)
851
852                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
853                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
854
855                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
856                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
857                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
858                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
859                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
860
861                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
862                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
863
864                info.extend([
865                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
866                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
867                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
868                    )),
869                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
870                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
871                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
872                    )),
873                    "| Changes between last deal price and last close              | {:<54} |\n".format(
874                        "{:.2f}%{}".format(
875                            iJSON["currentPrice"]["changes"],
876                            " ({}{:.2f} {})".format(
877                                "+" if bondChangesDelta > 0 else "",
878                                bondChangesDelta,
879                                aciCurrency
880                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
881                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
882                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
883                                currency
884                            ),
885                        )
886                    ),
887                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
888                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
889                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
890                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
891                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
892                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
893                    )),
894                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
895                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
896                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
897                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
898                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
899                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
900                    )),
901                ])
902
903            if "lot" in iJSON.keys():
904                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
905
906            if "step" in iJSON.keys() and iJSON["step"] != 0:
907                info.append("| Minimum price increment (step):                             | {:<54} |\n".format(iJSON["step"]))
908
909            # Add bond payment calendar:
910            if iJSON["type"] == "Bonds":
911                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
912                info.extend(["\n", strCalendar])
913
914            infoText += "".join(info)
915
916            if show:
917                uLogger.info("{}".format(infoText))
918
919            else:
920                uLogger.debug("{}".format(infoText))
921
922            if self.infoFile is not None:
923                with open(self.infoFile, "w", encoding="UTF-8") as fH:
924                    fH.write(infoText)
925
926                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
927
928        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker( self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
930    def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
931        """
932        Search and return raw broker's information about instrument by its ticker.
933        `ticker` must be defined! If debug=True then print all debug messages.
934
935        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
936        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
937        :param debug: if `True` then print all debug console messages.
938        :return: JSON formatted data with information about instrument.
939        """
940        tickerJSON = {}
941        if debug:
942            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
943
944        if not self.ticker:
945            uLogger.warning("self.ticker variable is not be empty!")
946
947        else:
948            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
949                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
950                raise Exception("Instrument not allowed")
951
952            if not self.iList:
953                self.iList = self.Listing()
954
955            if self.ticker in self.iList["Shares"].keys():
956                tickerJSON = self.iList["Shares"][self.ticker]
957                if debug:
958                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
959
960            elif self.ticker in self.iList["Currencies"].keys():
961                tickerJSON = self.iList["Currencies"][self.ticker]
962                if debug:
963                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
964
965            elif self.ticker in self.iList["Bonds"].keys():
966                tickerJSON = self.iList["Bonds"][self.ticker]
967                if debug:
968                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
969
970            elif self.ticker in self.iList["Etfs"].keys():
971                tickerJSON = self.iList["Etfs"][self.ticker]
972                if debug:
973                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
974
975            elif self.ticker in self.iList["Futures"].keys():
976                tickerJSON = self.iList["Futures"][self.ticker]
977                if debug:
978                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
979
980        if tickerJSON:
981            self.figi = tickerJSON["figi"]
982
983            if requestPrice:
984                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
985
986                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
987                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
988
989                else:
990                    tickerJSON["currentPrice"]["changes"] = 0
991
992            if show:
993                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
994
995        else:
996            if show:
997                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
998
999        return tickerJSON

Search and return raw broker's information about instrument by its ticker. ticker must be defined! If debug=True then print all debug messages.

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
  • debug: if True then print all debug console messages.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI( self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1001    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict:
1002        """
1003        Search and return raw broker's information about instrument by its FIGI.
1004        `figi` must be defined! If debug=True then print all debug messages.
1005
1006        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
1007        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
1008        :param debug: if `True` then print all debug console messages.
1009        :return: JSON formatted data with information about instrument.
1010        """
1011        figiJSON = {}
1012        if debug:
1013            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
1014
1015        if not self.figi:
1016            uLogger.warning("self.figi variable is not be empty!")
1017
1018        else:
1019            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
1020                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
1021                raise Exception("Instrument not allowed")
1022
1023            if not self.iList:
1024                self.iList = self.Listing()
1025
1026            for item in self.iList["Shares"].keys():
1027                if self.figi == self.iList["Shares"][item]["figi"]:
1028                    figiJSON = self.iList["Shares"][item]
1029
1030                    if debug:
1031                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
1032
1033                    break
1034
1035            if not figiJSON:
1036                for item in self.iList["Currencies"].keys():
1037                    if self.figi == self.iList["Currencies"][item]["figi"]:
1038                        figiJSON = self.iList["Currencies"][item]
1039
1040                        if debug:
1041                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
1042
1043                        break
1044
1045            if not figiJSON:
1046                for item in self.iList["Bonds"].keys():
1047                    if self.figi == self.iList["Bonds"][item]["figi"]:
1048                        figiJSON = self.iList["Bonds"][item]
1049
1050                        if debug:
1051                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
1052
1053                        break
1054
1055            if not figiJSON:
1056                for item in self.iList["Etfs"].keys():
1057                    if self.figi == self.iList["Etfs"][item]["figi"]:
1058                        figiJSON = self.iList["Etfs"][item]
1059
1060                        if debug:
1061                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
1062
1063                        break
1064
1065            if not figiJSON:
1066                for item in self.iList["Futures"].keys():
1067                    if self.figi == self.iList["Futures"][item]["figi"]:
1068                        figiJSON = self.iList["Futures"][item]
1069
1070                        if debug:
1071                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
1072
1073                        break
1074
1075        if figiJSON:
1076            self.figi = figiJSON["figi"]
1077            self.ticker = figiJSON["ticker"]
1078
1079            if requestPrice:
1080                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1081
1082                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1083                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1084
1085                else:
1086                    figiJSON["currentPrice"]["changes"] = 0
1087
1088            if show:
1089                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1090
1091        else:
1092            if show:
1093                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
1094
1095        return figiJSON

Search and return raw broker's information about instrument by its FIGI. figi must be defined! If debug=True then print all debug messages.

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
  • debug: if True then print all debug console messages.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1097    def GetCurrentPrices(self, show: bool = True) -> dict:
1098        """
1099        Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record:
1100        `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1101
1102        See also:
1103
1104        :param show: if `True` then print DOM to log and console.
1105        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1106        """
1107        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1108
1109        if self.depth < 1:
1110            uLogger.error("Depth of Market (DOM) must be >=1!")
1111            raise Exception("Incorrect value")
1112
1113        if not (self.ticker or self.figi):
1114            uLogger.error("self.ticker or self.figi variables must be defined!")
1115            raise Exception("Ticker or FIGI required")
1116
1117        if self.ticker and not self.figi:
1118            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1119            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1120
1121        if not self.ticker and self.figi:
1122            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1123            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1124
1125        if not self.figi:
1126            uLogger.error("FIGI is not defined!")
1127            raise Exception("Ticker or FIGI required")
1128
1129        else:
1130            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1131
1132            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1133            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1134            self.body = str({"figi": self.figi, "depth": self.depth})
1135            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")
1136
1137            if pricesResponse:
1138                # list of dicts with sellers orders:
1139                prices["buy"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1140
1141                # list of dicts with buyers orders:
1142                prices["sell"] = [{"price": NanoToFloat(item["price"]["units"], item["price"]["nano"]), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1143
1144                # max price of instrument at this time:
1145                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1146
1147                # min price of instrument at this time:
1148                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1149
1150                # last price of deal with instrument:
1151                prices["lastPrice"] = NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]) if "lastPrice" in pricesResponse.keys() else 0
1152
1153                # last close price of instrument:
1154                prices["closePrice"] = NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]) if "closePrice" in pricesResponse.keys() else 0
1155
1156            else:
1157                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1158                uLogger.debug("Server response: {}".format(pricesResponse))
1159
1160            if show:
1161                if prices["buy"] or prices["sell"]:
1162                    info = [
1163                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1164                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1165                            self.ticker,
1166                            self.figi,
1167                            self.depth,
1168                        ),
1169                        uLog.sepShort, "\n",
1170                        " Orders of Buyers   | Orders of Sellers\n",
1171                        uLog.sepShort, "\n",
1172                        " Sell prices (vol.) | Buy prices (vol.)\n",
1173                        uLog.sepShort, "\n",
1174                    ]
1175
1176                    if not prices["buy"]:
1177                        info.append("                    | No orders!\n")
1178                        sumBuy = 0
1179
1180                    else:
1181                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1182                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1183                        for item in maxMinSorted:
1184                            info.append("                    | {} ({})\n".format(item["price"], item["quantity"]))
1185
1186                    if not prices["sell"]:
1187                        info.append("No orders!          |\n")
1188                        sumSell = 0
1189
1190                    else:
1191                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1192                        for item in prices["sell"]:
1193                            info.append("{:>19} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1194
1195                    info.extend([
1196                        uLog.sepShort, "\n",
1197                        "{:>19} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1198                        uLog.sepShort, "\n",
1199                    ])
1200
1201                    infoText = "".join(info)
1202
1203                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1204
1205                else:
1206                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1207
1208        return prices

Get and show Depth of Market with current prices of the instrument. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

See also:

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1210    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1211        """
1212        This method get and show information about all available broker instruments for current user account.
1213        If `instrumentsFile` string is not empty then also save information to this file.
1214
1215        :param show: if `True` then print results to console, if `False` - print only to file.
1216        :return: multi-lines string with all available broker instruments
1217        """
1218        if not self.iList:
1219            self.iList = self.Listing()
1220
1221        info = [
1222            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1223            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1224        ]
1225
1226        # add instruments count by type:
1227        for iType in self.iList.keys():
1228            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1229
1230        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1231        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1232
1233        # generating info tables with all instruments by type:
1234        for iType in self.iList.keys():
1235            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1236
1237            for instrument in self.iList[iType].keys():
1238                iName = self.iList[iType][instrument]["name"]  # instrument's name
1239                if len(iName) > 57:
1240                    iName = "{}...".format(iName[:54])  # right trim for a long string
1241
1242                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1243                    self.iList[iType][instrument]["ticker"],
1244                    iName,
1245                    self.iList[iType][instrument]["figi"],
1246                    self.iList[iType][instrument]["currency"],
1247                    self.iList[iType][instrument]["lot"],
1248                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1249                ))
1250
1251        infoText = "".join(info)
1252
1253        if show:
1254            uLogger.info(infoText)
1255
1256        if self.instrumentsFile:
1257            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1258                fH.write(infoText)
1259
1260            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1261
1262        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False - print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1264    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1265        """
1266        This method search and show information about instruments by part of its ticker, FIGI or name.
1267        If `searchResultsFile` string is not empty then also save information to this file.
1268
1269        :param pattern: string with part of ticker, FIGI or instrument's name.
1270        :param show: if `True` then print results to console, if `False` - return list of result only.
1271        :return: list of dictionaries with all found instruments.
1272        """
1273        if not self.iList:
1274            self.iList = self.Listing()
1275
1276        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1277        compiledPattern = re.compile(pattern, re.IGNORECASE)
1278
1279        for iType in self.iList:
1280            for instrument in self.iList[iType].values():
1281                searchResult = compiledPattern.search(" ".join(
1282                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1283                ))
1284
1285                if searchResult:
1286                    searchResults[iType][instrument["ticker"]] = instrument
1287
1288        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1289        info = [
1290            "# Search results\n\n",
1291            "* **Search pattern:** [{}]\n".format(pattern),
1292            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1293            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1294        ]
1295        infoShort = info[:]
1296
1297        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1298        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1299        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1300
1301        if resultsLen == 0:
1302            info.append("\nNo results\n")
1303            infoShort.append("\nNo results\n")
1304            uLogger.warning("No results. Try changing your search pattern.")
1305
1306        else:
1307            for iType in searchResults:
1308                iTypeValuesCount = len(searchResults[iType].values())
1309                if iTypeValuesCount > 0:
1310                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1312
1313                    for instrument in searchResults[iType].values():
1314                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1315                            instrument["type"],
1316                            instrument["ticker"],
1317                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1318                            instrument["figi"],
1319                        ))
1320
1321                    if iTypeValuesCount <= 5:
1322                        infoShort.extend(info[-iTypeValuesCount:])
1323
1324                    else:
1325                        infoShort.extend(info[-5:])
1326                        infoShort.append(skippedLine)
1327
1328        infoText = "".join(info)
1329        infoTextShort = "".join(infoShort)
1330
1331        if show:
1332            uLogger.info(infoTextShort)
1333            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1334
1335        if self.searchResultsFile:
1336            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1337                fH.write(infoText)
1338
1339            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1340
1341        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False - return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1343    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1344        """
1345        Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
1346
1347        :param instruments: list of strings with tickers or FIGIs.
1348        :return: list with unique instrument FIGIs only.
1349        """
1350        requestedInstruments = []
1351        for iName in instruments:
1352            if iName not in self.aliases.keys():
1353                if iName not in requestedInstruments:
1354                    requestedInstruments.append(iName)
1355
1356            else:
1357                if iName not in requestedInstruments:
1358                    if self.aliases[iName] not in requestedInstruments:
1359                        requestedInstruments.append(self.aliases[iName])
1360
1361        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1362
1363        onlyUniqueFIGIs = []
1364        for iName in requestedInstruments:
1365            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1366                continue
1367
1368            self.ticker = iName
1369            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1370
1371            if not iData:
1372                self.ticker = ""
1373                self.figi = iName
1374
1375                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1376
1377                if not iData:
1378                    self.figi = ""
1379                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1380
1381            if iData and iData["figi"] not in onlyUniqueFIGIs:
1382                onlyUniqueFIGIs.append(iData["figi"])
1383
1384        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1385
1386        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1388    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1389        """
1390        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1391        See limits: https://tinkoff.github.io/investAPI/limits/
1392        If `pricesFile` string is not empty then also save information to this file.
1393
1394        :param instruments: list of strings with tickers or FIGIs.
1395        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1396        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1397                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1398        """
1399        if instruments is None or not instruments:
1400            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1401            raise Exception("Ticker or FIGI required")
1402
1403        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1404
1405        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1406
1407        iList = []  # trying to get info and current prices about all unique instruments:
1408        for self.figi in onlyUniqueFIGIs:
1409            iData = self.SearchByFIGI(requestPrice=True)
1410            iList.append(iData)
1411
1412        self.ShowListOfPrices(iList, show)
1413
1414        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! See limits: https://tinkoff.github.io/investAPI/limits/ If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1416    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1417        """
1418        Show table contains current prices of given instruments.
1419
1420        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1421                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1422        :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`.
1423        :return: multilines text in Markdown format as a table contains current prices.
1424        """
1425        infoText = ""
1426
1427        if show or self.pricesFile:
1428            info = [
1429                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1430                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1431                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1432            ]
1433
1434            for item in iList:
1435                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1436                    item["ticker"],
1437                    item["figi"],
1438                    item["type"],
1439                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1440                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1441                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1442                    "{} / {}".format(
1443                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1444                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1445                    ),
1446                    "{} / {}".format(
1447                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1448                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1449                    ),
1450                    item["currency"],
1451                ))
1452
1453            infoText = "".join(info)
1454
1455            if show:
1456                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1457
1458            if self.pricesFile:
1459                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1460                    fH.write(infoText)
1461
1462                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1463
1464        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False - prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1466    def RequestTradingStatus(self) -> dict:
1467        """
1468        Requesting trading status for the instrument defined by `figi` variable.
1469        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1470        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1471
1472        :return: dictionary with trading status attributes. Response example:
1473                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1474                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1475        """
1476        if self.figi is None or not self.figi:
1477            uLogger.error("Variable `figi` must be defined for using this method!")
1478            raise Exception("FIGI required")
1479
1480        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1481
1482        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1483        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1484        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1485
1486        uLogger.debug("Records about current trading status successfully received")
1487
1488        return tradingStatus

Requesting trading status for the instrument defined by figi variable. REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1490    def RequestPortfolio(self) -> dict:
1491        """
1492        Requesting actual user's portfolio for current `accountId`.
1493        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1494        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1495
1496        :return: dictionary with user's portfolio.
1497        """
1498        if self.accountId is None or not self.accountId:
1499            uLogger.error("Variable `accountId` must be defined for using this method!")
1500            raise Exception("Account ID required")
1501
1502        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1503
1504        self.body = str({"accountId": self.accountId})
1505        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1506        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1507
1508        uLogger.debug("Records about user's portfolio successfully received")
1509
1510        return rawPortfolio

Requesting actual user's portfolio for current accountId. REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1512    def RequestPositions(self) -> dict:
1513        """
1514        Requesting open positions by currencies and instruments for current `accountId`.
1515        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1516        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1517
1518        :return: dictionary with open positions by instruments.
1519        """
1520        if self.accountId is None or not self.accountId:
1521            uLogger.error("Variable `accountId` must be defined for using this method!")
1522            raise Exception("Account ID required")
1523
1524        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1525
1526        self.body = str({"accountId": self.accountId})
1527        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1528        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1529
1530        uLogger.debug("Records about current open positions successfully received")
1531
1532        return rawPositions

Requesting open positions by currencies and instruments for current accountId. REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1534    def RequestPendingOrders(self) -> list:
1535        """
1536        Requesting current actual pending orders for current `accountId`.
1537        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1538        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1539
1540        :return: list of dictionaries with pending orders.
1541        """
1542        if self.accountId is None or not self.accountId:
1543            uLogger.error("Variable `accountId` must be defined for using this method!")
1544            raise Exception("Account ID required")
1545
1546        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1547
1548        self.body = str({"accountId": self.accountId})
1549        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1550        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1551
1552        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1553
1554        return rawOrders

Requesting current actual pending orders for current accountId. REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1556    def RequestStopOrders(self) -> list:
1557        """
1558        Requesting current actual stop orders for current `accountId`.
1559        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1560        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1561
1562        :return: list of dictionaries with stop orders.
1563        """
1564        if self.accountId is None or not self.accountId:
1565            uLogger.error("Variable `accountId` must be defined for using this method!")
1566            raise Exception("Account ID required")
1567
1568        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1569
1570        self.body = str({"accountId": self.accountId})
1571        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1572        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1573
1574        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1575
1576        return rawStopOrders

Requesting current actual stop orders for current accountId. REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1578    def Overview(self, show: bool = False, details: str = "full") -> dict:
1579        """
1580        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1581        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1582        are defined then also save information to file.
1583
1584        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1585        many requests about the state of the portfolio, and then, based on the received data, a large number
1586        of calculation and statistics are collected.
1587
1588        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1589        :param details: how detailed should the information be? You should specify one of strings:
1590                        `full` - shows full available information about portfolio status (by default),
1591                        `positions` - shows only open positions,
1592                        `digest` - show a short digest of the portfolio status,
1593                        `analytics` - shows only the analytics section and the distribution of the portfolio by various categories,
1594                        `orders` - shows only sections of open limits and stop orders.
1595        :return: dictionary with client's raw portfolio and some statistics.
1596        """
1597        if self.accountId is None or not self.accountId:
1598            uLogger.error("Variable `accountId` must be defined for using this method!")
1599            raise Exception("Account ID required")
1600
1601        view = {
1602            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1603                "headers": {},  # list of dictionaries, response headers without "positions" section
1604                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1605                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1606                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1607                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1608                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1609                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1610                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1611                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1612                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1613            },
1614            "stat": {  # --- some statistics calculated using "raw" sections:
1615                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1616                "availableRUB": 0.,  # available rubles (without other currencies)
1617                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1618                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1619                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1620                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1621                "sharesCostRUB": 0.,  # costs of all shares in RUB
1622                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1623                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1624                "futuresCostRUB": 0.,  # costs of all futures in RUB
1625                "Currencies": [],  # list of dictionaries of all currencies statistics
1626                "Shares": [],  # list of dictionaries of all shares statistics
1627                "Bonds": [],  # list of dictionaries of all bonds statistics
1628                "Etfs": [],  # list of dictionaries of all etfs statistics
1629                "Futures": [],  # list of dictionaries of all futures statistics
1630                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1631                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1632                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1633                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1634                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1635            },
1636            "analytics": {  # --- some analytics of portfolio:
1637                "distrByAssets": {},  # portfolio distribution by assets
1638                "distrByCompanies": {},  # portfolio distribution by companies
1639                "distrBySectors": {},  # portfolio distribution by sectors
1640                "distrByCurrencies": {},  # portfolio distribution by currencies
1641                "distrByCountries": {},  # portfolio distribution by countries
1642            }
1643        }
1644
1645        details = details.lower()
1646        availableDetails = ["full", "positions", "digest", "analytics", "orders"]
1647        if details not in availableDetails:
1648            details = "full"
1649            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1650
1651        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1652
1653        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1654        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1655        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1656        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1657
1658        # save response headers without "positions" section:
1659        for key in portfolioResponse.keys():
1660            if key != "positions":
1661                view["raw"]["headers"][key] = portfolioResponse[key]
1662
1663            else:
1664                continue
1665
1666        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1667        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1668        for item in portfolioResponse["positions"]:
1669            if item["instrumentType"] == "currency":
1670                self.figi = item["figi"]
1671                curr = self.SearchByFIGI(requestPrice=False)
1672
1673                # current price of currency in RUB:
1674                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1675                    "name": curr["name"],
1676                    "currentPrice": NanoToFloat(
1677                        item["currentPrice"]["units"],
1678                        item["currentPrice"]["nano"]
1679                    ),
1680                }
1681
1682                view["raw"]["Currencies"].append(item)
1683
1684            elif item["instrumentType"] == "share":
1685                view["raw"]["Shares"].append(item)
1686
1687            elif item["instrumentType"] == "bond":
1688                view["raw"]["Bonds"].append(item)
1689
1690            elif item["instrumentType"] == "etf":
1691                view["raw"]["Etfs"].append(item)
1692
1693            elif item["instrumentType"] == "futures":
1694                view["raw"]["Futures"].append(item)
1695
1696            else:
1697                continue
1698
1699        # how many volume of currencies (by ISO currency name) are blocked:
1700        for item in view["raw"]["positions"]["blocked"]:
1701            blocked = NanoToFloat(item["units"], item["nano"])
1702            if blocked > 0:
1703                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1704
1705        # how many volume of instruments (by FIGI) are blocked:
1706        for item in view["raw"]["positions"]["securities"]:
1707            blocked = int(item["blocked"])
1708            if blocked > 0:
1709                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1710
1711        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1712
1713        if "rub" in allBlocked.keys():
1714            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1715
1716        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1717        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1718        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1719        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1720        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1721        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1722        view["stat"]["portfolioCostRUB"] = sum([
1723            view["stat"]["allCurrenciesCostRUB"],
1724            view["stat"]["sharesCostRUB"],
1725            view["stat"]["bondsCostRUB"],
1726            view["stat"]["etfsCostRUB"],
1727            view["stat"]["futuresCostRUB"],
1728        ])
1729
1730        # --- calculating some portfolio statistics:
1731        byComp = {}  # distribution by companies
1732        bySect = {}  # distribution by sectors
1733        byCurr = {}  # distribution by currencies (include RUB)
1734        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1735        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1736
1737        for item in portfolioResponse["positions"]:
1738            self.figi = item["figi"]
1739            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1740
1741            if instrument:
1742                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1743                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1744
1745                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1746                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1747
1748                else:
1749                    blocked = 0
1750
1751                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1752                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1753                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1754                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1755                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1756                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1757                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1758                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1759                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1760                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1761                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1762                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1763
1764                statData = {
1765                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1766                    "ticker": instrument["ticker"],  # ticker by FIGI
1767                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1768                    "volume": volume,  # available volume of instrument
1769                    "lots": lots,  # volume in lots of instrument
1770                    "direction": direction,  # direction of an instrument's position: short or long
1771                    "blocked": blocked,  # blocked volume of currency or instrument
1772                    "currentPrice": curPrice,  # current instrument's price in basic asset
1773                    "average": average,  # current average position price
1774                    "cost": cost,  # current cost of all volume of instrument in basic asset
1775                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1776                    "costRUB": costRUB,  # cost of instrument in ruble
1777                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1778                    "profit": profit,  # expected profit at current moment
1779                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1780                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1781                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1782                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1783                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1784                    "step": instrument["step"],  # minimum price increment
1785                }
1786
1787                # adding distribution by unique countries:
1788                if statData["country"] not in byCountry.keys():
1789                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1790
1791                else:
1792                    byCountry[statData["country"]]["cost"] += costRUB
1793                    byCountry[statData["country"]]["percent"] += percentCostRUB
1794
1795                if item["instrumentType"] != "currency":
1796                    # adding distribution by unique companies:
1797                    if statData["name"]:
1798                        if statData["name"] not in byComp.keys():
1799                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1800
1801                        else:
1802                            byComp[statData["name"]]["cost"] += costRUB
1803                            byComp[statData["name"]]["percent"] += percentCostRUB
1804
1805                    # adding distribution by unique sectors:
1806                    if statData["sector"] not in bySect.keys():
1807                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1808
1809                    else:
1810                        bySect[statData["sector"]]["cost"] += costRUB
1811                        bySect[statData["sector"]]["percent"] += percentCostRUB
1812
1813                # adding distribution by unique currencies:
1814                if currency not in byCurr.keys():
1815                    byCurr[currency] = {
1816                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1817                        "cost": costRUB,
1818                        "percent": percentCostRUB
1819                    }
1820
1821                else:
1822                    byCurr[currency]["cost"] += costRUB
1823                    byCurr[currency]["percent"] += percentCostRUB
1824
1825                # saving statistics for every instrument:
1826                if item["instrumentType"] == "currency":
1827                    view["stat"]["Currencies"].append(statData)
1828
1829                    # update dict with free funds for trading (total - blocked) by currencies
1830                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1831                    view["stat"]["funds"][currency] = {
1832                        "total": volume,
1833                        "totalCostRUB": costRUB,  # total volume cost in rubles
1834                        "free": volume - blocked,
1835                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1836                    }
1837
1838                elif item["instrumentType"] == "share":
1839                    view["stat"]["Shares"].append(statData)
1840
1841                elif item["instrumentType"] == "bond":
1842                    view["stat"]["Bonds"].append(statData)
1843
1844                elif item["instrumentType"] == "etf":
1845                    view["stat"]["Etfs"].append(statData)
1846
1847                elif item["instrumentType"] == "Futures":
1848                    view["stat"]["Futures"].append(statData)
1849
1850                else:
1851                    continue
1852
1853        # total changes in Russian Ruble:
1854        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1855        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1856        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1857        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1858        view["stat"]["funds"]["rub"] = {
1859            "total": view["stat"]["availableRUB"],
1860            "totalCostRUB": view["stat"]["availableRUB"],
1861            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1862            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1863        }
1864
1865        # --- pending orders sector data:
1866        uniquePendingOrders = []
1867        uniquePendingOrdersFIGIs = []
1868        for item in view["raw"]["orders"]:
1869            if item["figi"] not in uniquePendingOrdersFIGIs:
1870                uniquePendingOrdersFIGIs.append(item["figi"])
1871                uniquePendingOrders.append(item)
1872
1873        for item in uniquePendingOrders:
1874            self.figi = item["figi"]
1875            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1876
1877            if instrument:
1878                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1879                orderType = TKS_ORDER_TYPES[item["orderType"]]
1880                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1881                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1882
1883                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1884                if item["direction"] == "ORDER_DIRECTION_BUY":
1885                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1886
1887                else:
1888                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1889
1890                # requested price for order execution:
1891                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1892
1893                # necessary changes in percent to reach target from current price:
1894                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1895
1896                view["stat"]["orders"].append({
1897                    "orderID": item["orderId"],  # orderId number parameter of current order
1898                    "figi": item["figi"],  # FIGI identification
1899                    "ticker": instrument["ticker"],  # ticker name by FIGI
1900                    "lotsRequested": item["lotsRequested"],  # requested lots value
1901                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1902                    "currentPrice": lastPrice,  # current instrument's price for defined action
1903                    "targetPrice": target,  # requested price for order execution in base currency
1904                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1905                    "percentChanges": changes,  # changes in percent to target from current price
1906                    "currency": item["currency"],  # instrument's currency name
1907                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1908                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1909                    "status": orderState,  # order status from TKS_ORDER_STATES
1910                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1911                })
1912
1913        # --- stop orders sector data:
1914        uniqueStopOrders = []
1915        uniqueStopOrdersFIGIs = []
1916        for item in view["raw"]["stopOrders"]:
1917            if item["figi"] not in uniqueStopOrdersFIGIs:
1918                uniqueStopOrdersFIGIs.append(item["figi"])
1919                uniqueStopOrders.append(item)
1920
1921        for item in uniqueStopOrders:
1922            self.figi = item["figi"]
1923            instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI
1924
1925            if instrument:
1926                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1927                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1928                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1929
1930                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1931                if "expirationTime" in item.keys():
1932                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1933                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1934
1935                else:
1936                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1937                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1938
1939                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1940                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1941                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1942
1943                else:
1944                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1945
1946                # requested price when stop-order executed:
1947                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1948
1949                # price for limit-order, set up when stop-order executed:
1950                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1951
1952                # necessary changes in percent to reach target from current price:
1953                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1954
1955                view["stat"]["stopOrders"].append({
1956                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1957                    "figi": item["figi"],  # FIGI identification
1958                    "ticker": instrument["ticker"],  # ticker name by FIGI
1959                    "lotsRequested": item["lotsRequested"],  # requested lots value
1960                    "currentPrice": lastPrice,  # current instrument's price for defined action
1961                    "targetPrice": target,  # requested price for stop-order execution in base currency
1962                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1963                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1964                    "percentChanges": changes,  # changes in percent to target from current price
1965                    "currency": item["currency"],  # instrument's currency name
1966                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1967                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1968                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1969                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1970                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1971                })
1972
1973        # --- calculating data for analytics section:
1974        # portfolio distribution by assets:
1975        view["analytics"]["distrByAssets"] = {
1976            "Ruble": {
1977                "uniques": 1,
1978                "cost": view["stat"]["availableRUB"],
1979                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1980            },
1981            "Currencies": {
1982                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1983                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1984                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1985            },
1986            "Shares": {
1987                "uniques": len(view["stat"]["Shares"]),
1988                "cost": view["stat"]["sharesCostRUB"],
1989                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1990            },
1991            "Bonds": {
1992                "uniques": len(view["stat"]["Bonds"]),
1993                "cost": view["stat"]["bondsCostRUB"],
1994                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1995            },
1996            "Etfs": {
1997                "uniques": len(view["stat"]["Etfs"]),
1998                "cost": view["stat"]["etfsCostRUB"],
1999                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2000            },
2001            "Futures": {
2002                "uniques": len(view["stat"]["Futures"]),
2003                "cost": view["stat"]["futuresCostRUB"],
2004                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2005            },
2006        }
2007
2008        # portfolio distribution by companies:
2009        view["analytics"]["distrByCompanies"]["All money cash"] = {
2010            "ticker": "",
2011            "cost": view["stat"]["allCurrenciesCostRUB"],
2012            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2013        }
2014        view["analytics"]["distrByCompanies"].update(byComp)
2015
2016        # portfolio distribution by sectors:
2017        view["analytics"]["distrBySectors"]["All money cash"] = {
2018            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2019            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2020        }
2021        view["analytics"]["distrBySectors"].update(bySect)
2022
2023        # portfolio distribution by currencies:
2024        view["analytics"]["distrByCurrencies"].update(byCurr)
2025        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2026        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2027
2028        # portfolio distribution by countries:
2029        view["analytics"]["distrByCountries"].update(byCountry)
2030
2031        # --- Prepare text statistics overview in human-readable:
2032        if show:
2033            # Whatever the value `details`, header not changes:
2034            info = [
2035                "# Client's portfolio\n\n",
2036                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
2037                "* **Account ID:** [{}]\n".format(self.accountId),
2038            ]
2039
2040            if details in ["full", "positions", "digest"]:
2041                info.extend([
2042                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2043                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2044                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2045                        view["stat"]["totalChangesRUB"],
2046                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2047                        view["stat"]["totalChangesPercentRUB"],
2048                    ),
2049                ])
2050
2051            if details in ["full", "positions"]:
2052                info.extend([
2053                    "## Open positions\n\n",
2054                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2055                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2056                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2057                        "{:.2f} ({:.2f}) rub".format(
2058                            view["stat"]["availableRUB"],
2059                            view["stat"]["blockedRUB"],
2060                        )
2061                    )
2062                ])
2063
2064                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2065                    return [
2066                        "|                             |                                 |          |              |              |                     |                              |\n",
2067                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2068                            noTradeStr if noTradeStr else typeStr,
2069                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2070                        ),
2071                    ]
2072
2073                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2074                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2075                        "{} [{}]".format(data["ticker"], data["figi"]),
2076                        "{:.2f} ({:.2f}) {}".format(
2077                            data["volume"],
2078                            data["blocked"],
2079                            data["currency"],
2080                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2081                            data["volume"],
2082                            data["blocked"],
2083                        ),
2084                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2085                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2086                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2087                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2088                        "{}{:.2f} {} ({}{:.2f}%)".format(
2089                            "+" if data["profit"] > 0 else "",
2090                            data["profit"], data["baseCurrencyName"],
2091                            "+" if data["percentProfit"] > 0 else "",
2092                            data["percentProfit"],
2093                        ),
2094                    )
2095
2096                # --- Show currencies section:
2097                if view["stat"]["Currencies"]:
2098                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2099                    for item in view["stat"]["Currencies"]:
2100                        info.append(_InfoStr(item, showCurrencyName=True))
2101
2102                else:
2103                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2104
2105                # --- Show shares section:
2106                if view["stat"]["Shares"]:
2107                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2108
2109                    for item in view["stat"]["Shares"]:
2110                        info.append(_InfoStr(item))
2111
2112                else:
2113                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2114
2115                # --- Show bonds section:
2116                if view["stat"]["Bonds"]:
2117                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2118
2119                    for item in view["stat"]["Bonds"]:
2120                        info.append(_InfoStr(item))
2121
2122                else:
2123                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2124
2125                # --- Show etfs section:
2126                if view["stat"]["Etfs"]:
2127                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2128
2129                    for item in view["stat"]["Etfs"]:
2130                        info.append(_InfoStr(item))
2131
2132                else:
2133                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2134
2135                # --- Show futures section:
2136                if view["stat"]["Futures"]:
2137                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2138
2139                    for item in view["stat"]["Futures"]:
2140                        info.append(_InfoStr(item))
2141
2142                else:
2143                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2144
2145            if details in ["full", "orders"]:
2146                # --- Show pending orders section:
2147                if view["stat"]["orders"]:
2148                    info.extend([
2149                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2150                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2151                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2152                    ])
2153
2154                    for item in view["stat"]["orders"]:
2155                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2156                            "{} [{}]".format(item["ticker"], item["figi"]),
2157                            item["orderID"],
2158                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2159                            "{} {} ({}{:.2f}%)".format(
2160                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2161                                item["baseCurrencyName"],
2162                                "+" if item["percentChanges"] > 0 else "",
2163                                float(item["percentChanges"]),
2164                            ),
2165                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2166                            item["action"],
2167                            item["type"],
2168                            item["date"],
2169                        ))
2170
2171                else:
2172                    info.append("\n## Total pending limit-orders: 0\n")
2173
2174                # --- Show stop orders section:
2175                if view["stat"]["stopOrders"]:
2176                    info.extend([
2177                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2178                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2179                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2180                    ])
2181
2182                    for item in view["stat"]["stopOrders"]:
2183                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2184                            "{} [{}]".format(item["ticker"], item["figi"]),
2185                            item["orderID"],
2186                            item["lotsRequested"],
2187                            "{} {} ({}{:.2f}%)".format(
2188                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2189                                item["baseCurrencyName"],
2190                                "+" if item["percentChanges"] > 0 else "",
2191                                float(item["percentChanges"]),
2192                            ),
2193                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2194                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2195                            item["action"],
2196                            item["type"],
2197                            item["expType"],
2198                            item["createDate"],
2199                            item["expDate"],
2200                        ))
2201
2202                else:
2203                    info.append("\n## Total stop-orders: 0\n")
2204
2205            if details in ["full", "analytics"]:
2206                # -- Show analytics section:
2207                if view["stat"]["portfolioCostRUB"] > 0:
2208                    info.extend([
2209                        "\n# Analytics\n"
2210                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2211                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2212                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2213                            view["stat"]["totalChangesRUB"],
2214                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2215                            view["stat"]["totalChangesPercentRUB"],
2216                        ),
2217                        "\n## Portfolio distribution by assets\n"
2218                        "\n| Type       | Uniques | Percent | Current cost       |\n",
2219                        "|------------|---------|---------|--------------------|\n",
2220                    ])
2221
2222                    for key in view["analytics"]["distrByAssets"].keys():
2223                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2224                            info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format(
2225                                key,
2226                                view["analytics"]["distrByAssets"][key]["uniques"],
2227                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2228                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2229                            ))
2230
2231                    maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()])
2232                    info.extend([
2233                        "\n## Portfolio distribution by companies\n"
2234                        "\n| Company{} | Percent | Current cost       |\n".format(" " * (maxLenNames - 7)),
2235                        "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)),
2236                    ])
2237
2238                    for company in view["analytics"]["distrByCompanies"].keys():
2239                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2240                            nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"])
2241                            info.append("| {} | {:<7} | {:<18} |\n".format(
2242                                "{}{}{}".format(
2243                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2244                                    company,
2245                                    "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)),
2246                                ),
2247                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2248                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2249                            ))
2250
2251                    maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()])
2252                    info.extend([
2253                        "\n## Portfolio distribution by sectors\n"
2254                        "\n| Sector{} | Percent | Current cost       |\n".format(" " * (maxLenSectors - 6)),
2255                        "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)),
2256                    ])
2257
2258                    for sector in view["analytics"]["distrBySectors"].keys():
2259                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2260                            info.append("| {}{} | {:<7} | {:<18} |\n".format(
2261                                sector,
2262                                "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)),
2263                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2264                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2265                            ))
2266
2267                    maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()])
2268                    info.extend([
2269                        "\n## Portfolio distribution by currencies\n"
2270                        "\n| Instruments currencies{} | Percent | Current cost       |\n".format(" " * (maxLenMoney - 22)),
2271                        "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)),
2272                    ])
2273
2274                    for curr in view["analytics"]["distrByCurrencies"].keys():
2275                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2276                            nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"])
2277                            info.append("| {} | {:<7} | {:<18} |\n".format(
2278                                "[{}] {}{}".format(
2279                                    curr,
2280                                    view["analytics"]["distrByCurrencies"][curr]["name"],
2281                                    "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen),
2282                                ),
2283                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2284                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2285                            ))
2286
2287                    maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()]))
2288                    info.extend([
2289                        "\n## Portfolio distribution by countries\n"
2290                        "\n| Assets by country{} | Percent | Current cost       |\n".format(" " * (maxLenCountry - 17)),
2291                        "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)),
2292                    ])
2293
2294                    for country in view["analytics"]["distrByCountries"].keys():
2295                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2296                            nameLen = len(country)
2297                            info.append("| {} | {:<7} | {:<18} |\n".format(
2298                                "{}{}".format(
2299                                    country,
2300                                    "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen),
2301                                ),
2302                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2303                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2304                            ))
2305
2306            infoText = "".join(info)
2307
2308            uLogger.info(infoText)
2309
2310            if details == "full" and self.overviewFile:
2311                filename = self.overviewFile
2312
2313            elif details == "digest" and self.overviewDigestFile:
2314                filename = self.overviewDigestFile
2315
2316            elif details == "positions" and self.overviewPositionsFile:
2317                filename = self.overviewPositionsFile
2318
2319            elif details == "orders" and self.overviewOrdersFile:
2320                filename = self.overviewOrdersFile
2321
2322            elif details == "analytics" and self.overviewAnalyticsFile:
2323                filename = self.overviewAnalyticsFile
2324
2325            else:
2326                filename = ""
2327
2328            if filename:
2329                with open(filename, "w", encoding="UTF-8") as fH:
2330                    fH.write(infoText)
2331
2332                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2333
2334        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be? You should specify one of strings: full - shows full available information about portfolio status (by default), positions - shows only open positions, digest - show a short digest of the portfolio status, analytics - shows only the analytics section and the distribution of the portfolio by various categories, orders - shows only sections of open limits and stop orders.
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2336    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple:
2337        """
2338        Returns history operations between two given dates for current `accountId`.
2339        If `reportFile` string is not empty then also save human-readable report.
2340        Shows some statistical data of closed positions.
2341
2342        :param start: see docstring in `GetDatesAsString()` method
2343        :param end: see docstring in `GetDatesAsString()` method
2344        :param show: if `True` then also prints all records to the console.
2345        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2346        :return: original list of dictionaries with history of deals records from API ("operations" key):
2347                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2348                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2349        """
2350        if self.accountId is None or not self.accountId:
2351            uLogger.error("Variable `accountId` must be defined for using this method!")
2352            raise Exception("Account ID required")
2353
2354        startDate, endDate = GetDatesAsString(start, end)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2355
2356        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2357
2358        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2359        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2360        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2361        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2362        customStat = {}  # custom statistics in additional to responseJSON
2363
2364        # --- output report in human-readable format:
2365        if show or self.reportFile:
2366            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2367            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2368            nextDay = ""
2369
2370            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2371
2372            if len(ops) > 0:
2373                customStat = {
2374                    "opsCount": 0,  # total operations count
2375                    "buyCount": 0,  # buy operations
2376                    "sellCount": 0,  # sell operations
2377                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2378                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2379                    "payIn": {"rub": 0.},  # Deposit brokerage account
2380                    "payOut": {"rub": 0.},  # Withdrawals
2381                    "divs": {"rub": 0.},  # Dividends income
2382                    "coupons": {"rub": 0.},  # Coupon's income
2383                    "brokerCom": {"rub": 0.},  # Service commissions
2384                    "serviceCom": {"rub": 0.},  # Service commissions
2385                    "marginCom": {"rub": 0.},  # Margin commissions
2386                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2387                }
2388
2389                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2390                for item in ops:
2391                    if item["state"] == "OPERATION_STATE_EXECUTED":
2392                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2393
2394                        # count buy operations:
2395                        if "_BUY" in item["operationType"]:
2396                            customStat["buyCount"] += 1
2397
2398                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2399                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2400
2401                            else:
2402                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2403
2404                        # count sell operations:
2405                        elif "_SELL" in item["operationType"]:
2406                            customStat["sellCount"] += 1
2407
2408                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2409                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2410
2411                            else:
2412                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2413
2414                        # count incoming operations:
2415                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2416                            if item["payment"]["currency"] in customStat["payIn"].keys():
2417                                customStat["payIn"][item["payment"]["currency"]] += payment
2418
2419                            else:
2420                                customStat["payIn"][item["payment"]["currency"]] = payment
2421
2422                        # count withdrawals operations:
2423                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2424                            if item["payment"]["currency"] in customStat["payOut"].keys():
2425                                customStat["payOut"][item["payment"]["currency"]] += payment
2426
2427                            else:
2428                                customStat["payOut"][item["payment"]["currency"]] = payment
2429
2430                        # count dividends income:
2431                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2432                            if item["payment"]["currency"] in customStat["divs"].keys():
2433                                customStat["divs"][item["payment"]["currency"]] += payment
2434
2435                            else:
2436                                customStat["divs"][item["payment"]["currency"]] = payment
2437
2438                        # count coupon's income:
2439                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2440                            if item["payment"]["currency"] in customStat["coupons"].keys():
2441                                customStat["coupons"][item["payment"]["currency"]] += payment
2442
2443                            else:
2444                                customStat["coupons"][item["payment"]["currency"]] = payment
2445
2446                        # count broker commissions:
2447                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2448                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2449                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2450
2451                            else:
2452                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2453
2454                        # count service commissions:
2455                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2456                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2457                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2458
2459                            else:
2460                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2461
2462                        # count margin commissions:
2463                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2464                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2465                                customStat["marginCom"][item["payment"]["currency"]] += payment
2466
2467                            else:
2468                                customStat["marginCom"][item["payment"]["currency"]] = payment
2469
2470                        # count withholding taxes:
2471                        elif "_TAX" in item["operationType"]:
2472                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2473                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2474
2475                            else:
2476                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2477
2478                        else:
2479                            continue
2480
2481                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2482
2483                # --- view "Actions" lines:
2484                info.extend([
2485                    "| 1                          | 2                             | 3                            | 4                    | 5                      |\n",
2486                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2487                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2488                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2489                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2490                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2491                    ),
2492                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2493                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2494                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2495                    ),
2496                ])
2497
2498                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2499                for key in opsKeys:
2500                    if key == "rub":
2501                        continue
2502
2503                    info.extend([
2504                        "|                            |                               | {:<28} |                      |                        |\n".format(
2505                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2506                        ),
2507                        "|                            |                               | {:<28} |                      |                        |\n".format(
2508                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2509                        ),
2510                    ])
2511
2512                info.append(splitLine1)
2513
2514                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2515                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2516                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2517                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2518                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2519                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2520                    )
2521
2522                # --- view "Payments" lines:
2523                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2524                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2525
2526                for key in paymentsKeys:
2527                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2528
2529                info.append(splitLine1)
2530
2531                # --- view "Commissions and taxes" lines:
2532                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2533                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2534
2535                for key in comKeys:
2536                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2537
2538                info.append(splitLine1)
2539
2540                info.extend([
2541                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2542                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2543                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2544                ])
2545
2546            else:
2547                info.append("Broker returned no operations during this period\n")
2548
2549            # --- view "Operations" section:
2550            for item in ops:
2551                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2552                    continue
2553
2554                else:
2555                    self.figi = item["figi"] if item["figi"] else ""
2556                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2557                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2558
2559                    # group of deals during one day:
2560                    if nextDay and item["date"].split("T")[0] != nextDay:
2561                        info.append(splitLine2)
2562                        nextDay = ""
2563
2564                    else:
2565                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2566
2567                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2568                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2569                        self.figi if self.figi else "—",
2570                        instrument["ticker"] if instrument else "—",
2571                        instrument["type"] if instrument else "—",
2572                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2573                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2574                        TKS_OPERATION_STATES[item["state"]],
2575                        TKS_OPERATION_TYPES[item["operationType"]],
2576                    ))
2577
2578            infoText = "".join(info)
2579
2580            if show:
2581                uLogger.info(infoText)
2582
2583            if self.reportFile:
2584                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2585                    fH.write(infoText)
2586
2587                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2588
2589        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in GetDatesAsString() method
  • end: see docstring in GetDatesAsString() method
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2591    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2592        """
2593        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2594
2595        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2596        Warning! Broker server used ISO UTC time by default.
2597
2598        If `historyFile` is not `None` then method save history to file, otherwise return only pandas dataframe.
2599        Also, `historyFile` used to update history with `onlyMissing` parameter.
2600
2601        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2602
2603        :param start: see docstring in `GetDatesAsString()` method.
2604        :param end: see docstring in `GetDatesAsString()` method.
2605        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2606                         `"hour"`, `"day"`. Default: `"hour"`.
2607        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2608                            False by default. Warning! History appends only from last candle to current time
2609                            with always update last candle!
2610        :param csvSep: separator if csv-file is used, `,` by default.
2611        :param show: if `True` then also prints pandas dataframe to the console.
2612        :return: pandas dataframe with prices history. Headers of columns are defined by default:
2613                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2614        """
2615        strStartDate, strEndDate = GetDatesAsString(start, end)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2616        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2617        history = None  # empty pandas object for history
2618
2619        if interval not in TKS_CANDLE_INTERVALS.keys():
2620            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2621            raise Exception("Incorrect value")
2622
2623        if not (self.ticker or self.figi):
2624            uLogger.error("Ticker or FIGI must be defined!")
2625            raise Exception("Ticker or FIGI required")
2626
2627        if self.ticker and not self.figi:
2628            instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False)
2629            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2630
2631        if self.figi and not self.ticker:
2632            instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False)
2633            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2634
2635        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2636        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2637        if interval.lower() != "day":
2638            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59
2639
2640        delta = dtEnd - dtStart  # current UTC time minus last time in file
2641        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2642
2643        # calculate history length in candles:
2644        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2645        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2646            length += 1  # to avoid fraction time
2647
2648        # calculate data blocks count:
2649        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2650
2651        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2652        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2653        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2654        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2655        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2656
2657        tempOld = None  # pandas object for old history, if --only-missing key present
2658        lastTime = None  # datetime object of last old candle in file
2659
2660        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2661            uLogger.debug("--only-missing key present, add only last missing candles...")
2662            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2663
2664            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2665
2666            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2667            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2668            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2669            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2670
2671            # get last datetime object from last string in file or minus 1 delta if file is empty:
2672            if len(tempOld) > 0:
2673                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2674
2675            else:
2676                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2677
2678            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2679
2680        responseJSONs = []  # raw history blocks of data
2681
2682        blockEnd = dtEnd
2683        for item in range(blocks):
2684            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2685            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2686
2687            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2688                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2689            ))
2690
2691            if blockStart == blockEnd:
2692                uLogger.debug("Skipped this zero-length block...")
2693
2694            else:
2695                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2696                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2697                self.body = str({
2698                    "figi": self.figi,
2699                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2700                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2701                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2702                })
2703                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False)
2704
2705                if "code" in responseJSON.keys():
2706                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2707
2708                else:
2709                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2710                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2711
2712                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2713
2714            blockEnd = blockStart
2715
2716        printCount = len(responseJSONs)  # candles to show in console
2717        if responseJSONs:
2718            tempHistory = pd.DataFrame(
2719                data={
2720                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2721                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2722                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2723                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2724                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2725                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2726                    "volume": [int(item["volume"]) for item in responseJSONs],
2727                },
2728                index=range(len(responseJSONs)),
2729                columns=["date", "time", "open", "high", "low", "close", "volume"],
2730            )
2731            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2732            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2733
2734            # append only newest candles to old history if --only-missing key present:
2735            if onlyMissing and tempOld is not None and lastTime is not None:
2736                index = 0  # find start index in tempHistory data:
2737
2738                for i, item in tempHistory.iterrows():
2739                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2740
2741                    if curTime == lastTime:
2742                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2743                        index = i
2744                        printCount = index + 1
2745                        break
2746
2747                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2748
2749            else:
2750                history = tempHistory  # if no `--only-missing` key then load full data from server
2751
2752            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2753
2754        if history is not None and not history.empty:
2755            if show:
2756                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2757                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2758                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2759                ))
2760
2761        else:
2762            uLogger.warning("Received an empty candles history!")
2763
2764        if self.historyFile is not None:
2765            if history is not None and not history.empty:
2766                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2767                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2768
2769            else:
2770                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2771
2772        else:
2773            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only pandas dataframe returns.")
2774
2775        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only pandas dataframe. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in GetDatesAsString() method.
  • end: see docstring in GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints pandas dataframe to the console.
Returns

pandas dataframe with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2777    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2778        """
2779        Load candles history from csv-file and return pandas dataframe object.
2780
2781        See also: `History()` and `ShowHistoryChart()` methods.
2782
2783        :param filePath: path to csv-file to open.
2784        """
2785        loadedHistory = None  # init candles data object
2786
2787        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2788
2789        if os.path.exists(filePath):
2790            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as pandas dataframe
2791
2792            tfStr = self.priceModel.FormattedDelta(
2793                self.priceModel.timeframe,
2794                "{days} days {hours}h {minutes}m {seconds}s",
2795            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2796                self.priceModel.timeframe,
2797                "{hours}h {minutes}m {seconds}s",
2798            )
2799
2800            if loadedHistory is not None and not loadedHistory.empty:
2801                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2802                    len(loadedHistory),
2803                    tfStr,
2804                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2805                )
2806
2807            else:
2808                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2809
2810        else:
2811            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2812
2813        return loadedHistory

Load candles history from csv-file and return pandas dataframe object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2815    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2816        """
2817        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2818
2819        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2820        Default: `index.html` (both for interact and non-interact candlesticks chart).
2821
2822        See also: `History()` and `LoadHistory()` methods.
2823
2824        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2825        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2826                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2827                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2828                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2829        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2830                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2831        """
2832        if isinstance(candles, str):
2833            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2834            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2835
2836        elif isinstance(candles, pd.DataFrame):
2837            self.priceModel.prices = candles  # set candles chain from variable
2838            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2839
2840            if "datetime" not in candles.columns:
2841                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2842
2843        else:
2844            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2845            raise Exception("Incorrect value")
2846
2847        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2848
2849        if interact:
2850            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2851
2852            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2853
2854        else:
2855            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2856
2857            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2858
2859        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2861    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2862        """
2863        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2864        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2865
2866        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2867
2868        :param operation: string "Buy" or "Sell".
2869        :param lots: volume, integer count of lots >= 1.
2870        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2871        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2872        :param expDate: string "Undefined" by default or local date in future,
2873                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2874        :return: JSON with response from broker server.
2875        """
2876        if self.accountId is None or not self.accountId:
2877            uLogger.error("Variable `accountId` must be defined for using this method!")
2878            raise Exception("Account ID required")
2879
2880        if operation is None or not operation or operation not in ("Buy", "Sell"):
2881            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2882            raise Exception("Incorrect value")
2883
2884        if lots is None or lots < 1:
2885            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2886            lots = 1
2887
2888        if tp is None or tp < 0:
2889            tp = 0
2890
2891        if sl is None or sl < 0:
2892            sl = 0
2893
2894        if expDate is None or not expDate:
2895            expDate = "Undefined"
2896
2897        if not (self.ticker or self.figi):
2898            uLogger.error("Ticker or FIGI must be defined!")
2899            raise Exception("Ticker or FIGI required")
2900
2901        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
2902        self.ticker = instrument["ticker"]
2903        self.figi = instrument["figi"]
2904
2905        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2906
2907        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2908        self.body = str({
2909            "figi": self.figi,
2910            "quantity": str(lots),
2911            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2912            "accountId": str(self.accountId),
2913            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2914        })
2915        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False)
2916
2917        if "orderId" in response.keys():
2918            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2919                operation, response["orderId"],
2920                self.ticker, self.figi, lots,
2921                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2922                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2923                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2924            ))
2925
2926        else:
2927            uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.")
2928
2929        if tp > 0:
2930            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2931
2932        if sl > 0:
2933            self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2934
2935        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2937    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2938        """
2939        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2940        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2941
2942        See also: `Order()` and `Trade()` docstrings.
2943
2944        :param lots: volume, integer count of lots >= 1.
2945        :param tp: float > 0, take profit price of stop-order.
2946        :param sl: float > 0, stop loss price of stop-order.
2947        :param expDate: it's a local date in future.
2948                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2949        :return: JSON with response from broker server.
2950        """
2951        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2953    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2954        """
2955        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2956        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2957
2958        See also: `Order()` and `Trade()` docstrings.
2959
2960        :param lots: volume, integer count of lots >= 1.
2961        :param tp: float > 0, take profit price of stop-order.
2962        :param sl: float > 0, stop loss price of stop-order.
2963        :param expDate: it's a local date in the future.
2964                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2965        :return: JSON with response from broker server.
2966        """
2967        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2969    def CloseTrades(self, tickers: list, portfolio: dict = None) -> None:
2970        """
2971        Close position of given instruments.
2972
2973        :param tickers: tickers list of instruments that must be closed.
2974        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2975                         This avoids unnecessary downloading data from the server.
2976        """
2977        if not tickers:
2978            uLogger.info("Tickers list is empty, nothing to close.")
2979
2980        else:
2981            if portfolio is None or not portfolio:
2982                portfolio = self.Overview(show=False)
2983
2984            allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2985            uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers))
2986
2987            for ticker in tickers:
2988                if ticker not in allOpenedTickers:
2989                    uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker))
2990                    continue
2991
2992                # search open trade info about instrument by ticker:
2993                instrument = {}
2994                for iType in TKS_INSTRUMENTS:
2995                    if instrument:
2996                        break
2997
2998                    for item in portfolio["stat"][iType]:
2999                        if item["ticker"] == ticker:
3000                            instrument = item
3001                            break
3002
3003                if instrument:
3004                    self.ticker = ticker
3005                    self.figi = instrument["figi"]
3006
3007                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3008                        self.ticker,
3009                        self.figi,
3010                        int(instrument["volume"]),
3011                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3012                    ))
3013
3014                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3015
3016                    if tradeLots > 0:
3017                        if instrument["blocked"] > 0:
3018                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3019                                instrument["blocked"],
3020                                self.ticker,
3021                                tradeLots,
3022                            ))
3023
3024                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3025                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3026
3027                    else:
3028                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • tickers: tickers list of instruments that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3030    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3031        """
3032        Close all positions of given instruments with defined type.
3033
3034        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3035        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3036                         This avoids unnecessary downloading data from the server.
3037        """
3038        if iType not in TKS_INSTRUMENTS:
3039            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3040
3041        else:
3042            if portfolio is None or not portfolio:
3043                portfolio = self.Overview(show=False)
3044
3045            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3046            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3047
3048            if tickers and portfolio:
3049                self.CloseTrades(tickers, portfolio)
3050
3051            else:
3052                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3054    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3055        """
3056        Universal method to create market or limit orders with all available parameters for current `accountId`.
3057        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3058
3059        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3060        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3061
3062        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3063        then broker immediately open market order as you can do simple --buy or --sell operations!
3064
3065        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3066        When current price will go up or down to target price value then broker opens a limit order.
3067        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3068
3069        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3070
3071        :param operation: string "Buy" or "Sell".
3072        :param orderType: string "Limit" or "Stop".
3073        :param lots: volume, integer count of lots >= 1.
3074        :param targetPrice: target price > 0. This is open trade price for limit order.
3075        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3076                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3077        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3078                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3079                         Stop loss order always executed by market price.
3080        :param expDate: string "Undefined" by default or local date in future.
3081                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3082                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3083                        A limit order has no expiration date, it lasts until the end of the trading day.
3084        :return: JSON with response from broker server.
3085        """
3086        if self.accountId is None or not self.accountId:
3087            uLogger.error("Variable `accountId` must be defined for using this method!")
3088            raise Exception("Account ID required")
3089
3090        if operation is None or not operation or operation not in ("Buy", "Sell"):
3091            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3092            raise Exception("Incorrect value")
3093
3094        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3095            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3096            raise Exception("Incorrect value")
3097
3098        if lots is None or lots < 1:
3099            uLogger.error("You must define trade volume > 0: integer count of lots!")
3100            raise Exception("Incorrect value")
3101
3102        if targetPrice is None or targetPrice <= 0:
3103            uLogger.error("Target price for limit-order must be greater than 0!")
3104            raise Exception("Incorrect value")
3105
3106        if limitPrice is None or limitPrice <= 0:
3107            limitPrice = targetPrice
3108
3109        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3110            stopType = "Limit"
3111
3112        if expDate is None or not expDate:
3113            expDate = "Undefined"
3114
3115        if not (self.ticker or self.figi):
3116            uLogger.error("Tocker or FIGI must be defined!")
3117            raise Exception("Ticker or FIGI required")
3118
3119        response = {}
3120        instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False)
3121        self.ticker = instrument["ticker"]
3122        self.figi = instrument["figi"]
3123
3124        if orderType == "Limit":
3125            uLogger.debug(
3126                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3127                    self.ticker, self.figi,
3128                    operation, lots, targetPrice, instrument["currency"],
3129                ))
3130
3131            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3132            self.body = str({
3133                "figi": self.figi,
3134                "quantity": str(lots),
3135                "price": FloatToNano(targetPrice),
3136                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3137                "accountId": str(self.accountId),
3138                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3139            })
3140            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3141
3142            if "orderId" in response.keys():
3143                uLogger.info(
3144                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3145                        response["orderId"],
3146                        self.ticker, self.figi,
3147                        operation, lots, targetPrice, instrument["currency"],
3148                    ))
3149
3150                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3151                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3152                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3153                            targetPrice, instrument["currency"],
3154                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3155                        ))
3156
3157                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3158                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3159                            targetPrice, instrument["currency"],
3160                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3161                        ))
3162
3163            else:
3164                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3165
3166        if orderType == "Stop":
3167            uLogger.debug(
3168                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3169                    self.ticker, self.figi,
3170                    operation, lots,
3171                    targetPrice, instrument["currency"],
3172                    limitPrice, instrument["currency"],
3173                    stopType, expDate,
3174                ))
3175
3176            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3177            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3178            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3179
3180            body = {
3181                "figi": self.figi,
3182                "quantity": str(lots),
3183                "price": FloatToNano(limitPrice),
3184                "stopPrice": FloatToNano(targetPrice),
3185                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3186                "accountId": str(self.accountId),
3187                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3188                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3189            }
3190
3191            if expDateUTC:
3192                body["expireDate"] = expDateUTC
3193
3194            self.body = str(body)
3195            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False)
3196
3197            if "stopOrderId" in response.keys():
3198                uLogger.info(
3199                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3200                        response["stopOrderId"],
3201                        self.ticker, self.figi,
3202                        operation, lots,
3203                        targetPrice, instrument["currency"],
3204                        limitPrice, instrument["currency"],
3205                        TKS_STOP_ORDER_TYPES[stopOrderType],
3206                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3207                    ))
3208
3209                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3210                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3211                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3212                            targetPrice, instrument["currency"],
3213                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3214                        ))
3215
3216                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3217                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3218                            targetPrice, instrument["currency"],
3219                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3220                        ))
3221
3222            else:
3223                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3224
3225        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3227    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3228        """
3229        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3230        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3231        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3232        See also: `Order()` docstring.
3233
3234        :param lots: volume, integer count of lots >= 1.
3235        :param targetPrice: target price > 0. This is open trade price for limit order.
3236        :return: JSON with response from broker server.
3237        """
3238        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3240    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3241        """
3242        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3243        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3244        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3245        target price value then broker opens a limit order. See also: `Order()` docstring.
3246
3247        :param lots: volume, integer count of lots >= 1.
3248        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3249        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3250                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3251        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3252                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3253        :param expDate: string "Undefined" by default or local date in future.
3254                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3255                        This date is converting to UTC format for server.
3256        :return: JSON with response from broker server.
3257        """
3258        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3260    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3261        """
3262        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3263        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3264        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3265        See also: `Order()` docstring.
3266
3267        :param lots: volume, integer count of lots >= 1.
3268        :param targetPrice: target price > 0. This is open trade price for limit order.
3269        :return: JSON with response from broker server.
3270        """
3271        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3273    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3274        """
3275        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3276        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3277        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3278        target price value then broker opens a limit order. See also: `Order()` docstring.
3279
3280        :param lots: volume, integer count of lots >= 1.
3281        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3282        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3283                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3284        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3285                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3286        :param expDate: string "Undefined" by default or local date in future.
3287                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3288                        This date is converting to UTC format for server.
3289        :return: JSON with response from broker server.
3290        """
3291        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3293    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3294        """
3295        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3296
3297        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3298        :param allOrdersIDs: pre-received lists of all active pending orders.
3299                             This avoids unnecessary downloading data from the server.
3300        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3301        """
3302        if self.accountId is None or not self.accountId:
3303            uLogger.error("Variable `accountId` must be defined for using this method!")
3304            raise Exception("Account ID required")
3305
3306        if orderIDs:
3307            if allOrdersIDs is None or not allOrdersIDs:
3308                rawOrders = self.RequestPendingOrders()
3309                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3310
3311            if allStopOrdersIDs is None or not allStopOrdersIDs:
3312                rawStopOrders = self.RequestStopOrders()
3313                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3314
3315            for orderID in orderIDs:
3316                idInPendingOrders = orderID in allOrdersIDs
3317                idInStopOrders = orderID in allStopOrdersIDs
3318
3319                if not (idInPendingOrders or idInStopOrders):
3320                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3321                    continue
3322
3323                else:
3324                    if idInPendingOrders:
3325                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3326
3327                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3328                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3329                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3330                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3331
3332                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3333                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3334                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3335
3336                        else:
3337                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3338
3339                    elif idInStopOrders:
3340                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3341
3342                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3343                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3344                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3345                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3346
3347                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3348                            uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3349                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3350
3351                        else:
3352                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3353
3354                    else:
3355                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3357    def CloseAllOrders(self) -> None:
3358        """
3359        Gets a list of open pending and stop orders and cancel it all.
3360        """
3361        rawOrders = self.RequestPendingOrders()
3362        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3363        lenOrders = len(allOrdersIDs)
3364
3365        rawStopOrders = self.RequestStopOrders()
3366        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3367        lenSOrders = len(allStopOrdersIDs)
3368
3369        if lenOrders > 0 or lenSOrders > 0:
3370            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3371
3372            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3373
3374        else:
3375            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3377    def CloseAll(self, *args) -> None:
3378        """
3379        Close all available (not blocked) opened trades and orders.
3380
3381        Also, you can select one or more keywords case-insensitive:
3382        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3383
3384        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3385        """
3386        overview = self.Overview(show=False)  # get all open trades info
3387
3388        if len(args) == 0:
3389            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3390            self.CloseAllOrders()  # close all pending and stop orders
3391
3392            for iType in TKS_INSTRUMENTS:
3393                if iType != "Currencies":
3394                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3395
3396        else:
3397            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3398            lowerArgs = [x.lower() for x in args]
3399
3400            if "orders" in lowerArgs:
3401                self.CloseAllOrders()  # close all pending and stop orders
3402
3403            for iType in TKS_INSTRUMENTS:
3404                if iType.lower() in lowerArgs and iType != "Currencies":
3405                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3407    @staticmethod
3408    def ParseOrderParameters(operation, **inputParameters):
3409        """
3410        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3411
3412        :param operation: string "Buy" or "Sell".
3413        :param inputParameters: this is dict of strings that looks like this
3414               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3415               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3416               "prices" key: one or more prices to open limit-orders
3417               Counts of values in lots and prices lists must be equals!
3418        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3419        """
3420        # TODO: update order grid work with api v2
3421        pass
3422        # uLogger.debug("Input parameters: {}".format(inputParameters))
3423        #
3424        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3425        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3426        #     raise Exception("Incorrect value")
3427        #
3428        # if "l" in inputParameters.keys():
3429        #     inputParameters["lots"] = inputParameters.pop("l")
3430        #
3431        # if "p" in inputParameters.keys():
3432        #     inputParameters["prices"] = inputParameters.pop("p")
3433        #
3434        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3435        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3436        #     raise Exception("Incorrect value")
3437        #
3438        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3439        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3440        #
3441        # if len(lots) != len(prices):
3442        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3443        #     raise Exception("Incorrect value")
3444        #
3445        # uLogger.debug("Extracted parameters for orders:")
3446        # uLogger.debug("lots = {}".format(lots))
3447        # uLogger.debug("prices = {}".format(prices))
3448        #
3449        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3450        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3451        # uLogger.debug("Order parameters: {}".format(result))
3452        #
3453        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3455    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3456        """
3457        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3458
3459        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3460        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3461        """
3462        result = False
3463        msg = "Instrument not defined!"
3464
3465        if portfolio is None or not portfolio:
3466            portfolio = self.Overview(show=False)
3467
3468        if self.ticker:
3469            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3470            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3471
3472            for iType in TKS_INSTRUMENTS:
3473                for instrument in portfolio["stat"][iType]:
3474                    if instrument["ticker"] == self.ticker:
3475                        result = True
3476                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3477                        break
3478
3479        elif self.figi:
3480            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3481            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3482
3483            for iType in TKS_INSTRUMENTS:
3484                for instrument in portfolio["stat"][iType]:
3485                    if instrument["figi"] == self.figi:
3486                        result = True
3487                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3488                        break
3489
3490        else:
3491            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3492
3493        uLogger.debug(msg)
3494
3495        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3497    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3498        """
3499        Returns instrument is in the user's portfolio if it presents there.
3500        Instrument must be defined by `ticker` (highly priority) or `figi`.
3501
3502        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3503        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3504        """
3505        result = None
3506        msg = "Instrument not defined!"
3507
3508        if portfolio is None or not portfolio:
3509            portfolio = self.Overview(show=False)
3510
3511        if self.ticker:
3512            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3513            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3514
3515            for iType in TKS_INSTRUMENTS:
3516                for instrument in portfolio["stat"][iType]:
3517                    if instrument["ticker"] == self.ticker:
3518                        result = instrument
3519                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3520                        break
3521
3522        elif self.figi:
3523            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3524            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3525
3526            for iType in TKS_INSTRUMENTS:
3527                for instrument in portfolio["stat"][iType]:
3528                    if instrument["figi"] == self.figi:
3529                        result = instrument
3530                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3531                        break
3532
3533        else:
3534            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3535
3536        uLogger.debug(msg)
3537
3538        return result

Returns instrument is in the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3540    def RequestLimits(self) -> dict:
3541        """
3542        Method for obtaining the available funds for withdrawal for current `accountId`.
3543
3544        See also:
3545        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3546        - `OverviewLimits()` method
3547
3548        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3549                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3550                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3551                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3552        """
3553        if self.accountId is None or not self.accountId:
3554            uLogger.error("Variable `accountId` must be defined for using this method!")
3555            raise Exception("Account ID required")
3556
3557        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3558
3559        self.body = str({"accountId": self.accountId})
3560        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3561        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3562
3563        uLogger.debug("Records about available funds for withdrawal successfully received")
3564
3565        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3567    def OverviewLimits(self, show: bool = False) -> dict:
3568        """
3569        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3570
3571        See also: `RequestLimits()`.
3572
3573        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3574        :return: dict with raw parsed data from server and some calculated statistics about it.
3575        """
3576        if self.accountId is None or not self.accountId:
3577            uLogger.error("Variable `accountId` must be defined for using this method!")
3578            raise Exception("Account ID required")
3579
3580        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3581
3582        view = {
3583            "rawLimits": rawLimits,
3584            "limits": {  # parsed data for every currency:
3585                "money": {  # this is an array of portfolio currency positions
3586                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3587                },
3588                "blocked": {  # this is an array of blocked currency
3589                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3590                },
3591                "blockedGuarantee": {  # this is locked money under collateral for futures
3592                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3593                },
3594            },
3595        }
3596
3597        # --- Prepare text table with limits in human-readable format:
3598        if show:
3599            info = [
3600                "# Withdrawal limits\n\n",
3601                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3602                "* **Account ID:** [{}]\n".format(self.accountId),
3603                "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3604                "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3605            ]
3606
3607            for curr in view["limits"]["money"].keys():
3608                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3609                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3610                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3611
3612                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3613                    "[{}]".format(curr),
3614                    "{:.2f}".format(view["limits"]["money"][curr]),
3615                    "{:.2f}".format(availableMoney),
3616                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3617                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3618                )
3619
3620                if curr == "rub":
3621                    info.insert(5, infoStr)  # insert at first position in table and after headers
3622
3623                else:
3624                    info.append(infoStr)
3625
3626            infoText = "".join(info)
3627
3628            uLogger.info(infoText)
3629
3630            if self.withdrawalLimitsFile:
3631                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3632                    fH.write(infoText)
3633
3634                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3635
3636        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3638    def RequestAccounts(self) -> dict:
3639        """
3640        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3641
3642        See also:
3643        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3644        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3645        - `OverviewUserInfo()` method
3646
3647        :return: dict with raw data from server that contains accounts info. Example of dict:
3648                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3649                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3650                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3651                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3652        """
3653        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3654
3655        self.body = str({})
3656        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3657        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3658
3659        uLogger.debug("Records about available accounts successfully received")
3660
3661        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3663    def RequestUserInfo(self) -> dict:
3664        """
3665        Method for requesting common user's information.
3666
3667        See also:
3668        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3669        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3670        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3671        - `OverviewUserInfo()` method
3672
3673        :return: dict with raw data from server that contains user's information. Example of dict:
3674                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3675                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3676        """
3677        uLogger.debug("Requesting common user's information. Wait, please...")
3678
3679        self.body = str({})
3680        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3681        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3682
3683        uLogger.debug("Records about current user successfully received")
3684
3685        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3687    def RequestMarginStatus(self, accountId: str = None) -> dict:
3688        """
3689        Method for requesting margin calculation for defined account ID.
3690
3691        See also:
3692        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3693        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3694        - `OverviewUserInfo()` method
3695
3696        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3697        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3698                 Example of responses:
3699                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3700                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3701                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3702                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3703                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3704                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3705        """
3706        if accountId is None or not accountId:
3707            if self.accountId is None or not self.accountId:
3708                uLogger.error("Variable `accountId` must be defined for using this method!")
3709                raise Exception("Account ID required")
3710
3711            else:
3712                accountId = self.accountId  # use `self.accountId` (main ID) by default
3713
3714        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3715
3716        self.body = str({"accountId": accountId})
3717        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3718        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3719
3720        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3721            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3722            rawMargin = {}
3723
3724        else:
3725            uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3726
3727        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3729    def RequestTariffLimits(self) -> dict:
3730        """
3731        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3732
3733        See also:
3734        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3735        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3736        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3737        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3738        - `OverviewUserInfo()` method
3739
3740        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3741                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3742                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3743        """
3744        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3745
3746        self.body = str({})
3747        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3748        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3749
3750        uLogger.debug("Records with limits of current tariff successfully received")
3751
3752        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3754    def RequestBondCoupons(self, iJSON: dict) -> dict:
3755        """
3756        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3757        then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
3758        All dates are in UTC timezone.
3759
3760        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3761        Documentation:
3762        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3763        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3764
3765        See also: `ExtendBondsData()`.
3766
3767        :param iJSON: raw json data of a bond from broker server, example: `iJSON = self.iList["Bonds"][self.ticker]`
3768                      If raw iJSON is not data of bond then server returns an error [400] with message:
3769                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3770        :return: dictionary with bond payment calendar. Response example:
3771                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3772                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3773                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3774                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3775        """
3776        if iJSON["figi"] is None or not iJSON["figi"]:
3777            uLogger.error("FIGI must be defined for using this method!")
3778            raise Exception("FIGI required")
3779
3780        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3781        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3782
3783        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3784            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3785            self.figi,
3786            startDate,
3787            endDate,
3788        ))
3789
3790        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3791        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3792        calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False)
3793
3794        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3795            uLogger.warning("Instrument type is not bond!")
3796
3797        else:
3798            uLogger.debug("Records about bond payment calendar successfully received")
3799
3800        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example: iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example: {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3802    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3803        """
3804        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3805        pandas dataframe with more information about bonds: main info, current prices, bond payment calendar,
3806        coupon yields, current yields and some statistics etc.
3807
3808        WARNING! This is too long operation if a lot of bonds requested from broker server.
3809
3810        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3811
3812        :param instruments: list of strings with tickers or FIGIs.
3813        :param xlsx: if True then also exports pandas dataframe to xlsx-file `bondsXLSXFile`, default: `ext-bonds.xlsx`,
3814                     for further used by data scientists or stock analytics.
3815        :return: wider pandas dataframe with more full and calculated data about bonds, than raw response from broker.
3816                 In XLSX-file and pandas dataframe fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3817        """
3818        if instruments is None or not instruments:
3819            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3820            raise Exception("Ticker or FIGI required")
3821
3822        if isinstance(instruments, str):
3823            instruments = [instruments]
3824
3825        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3826
3827        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3828
3829        iCount = len(uniqueInstruments)
3830        tooLong = iCount >= 20
3831        if tooLong:
3832            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3833
3834        bonds = None
3835        for i, self.figi in enumerate(uniqueInstruments):
3836            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3837
3838            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3839                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3840                rawBond = self.SearchByFIGI(requestPrice=True)
3841
3842                # Widen raw data with UTC current time (iData["actualDateTime"]):
3843                actualDate = datetime.now(tzutc())
3844                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3845
3846                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3847                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3848
3849                # Replace some values with human-readable:
3850                iData["nominalCurrency"] = iData["nominal"]["currency"]
3851                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3852                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3853                iData["aciCurrency"] = iData["aciValue"]["currency"]
3854                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3855                iData["issueSize"] = int(iData["issueSize"])
3856                iData["issueSizePlan"] = int(iData["issueSize"])
3857                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3858                iData["minPriceIncrement"] = NanoToFloat(iData["minPriceIncrement"]["units"], iData["minPriceIncrement"]["nano"]) if "minPriceIncrement" in iData.keys() else 0.
3859                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3860
3861                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3862                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3863                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3864                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3865                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3866                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3867                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3868                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3869                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3870                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3871                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3872
3873                # Widen raw data with calendar data from `rawCalendar` values:
3874                calendarData = []
3875                for item in iData["rawCalendar"]["events"]:
3876                    calendarData.append({
3877                        "couponDate": item["couponDate"],
3878                        "couponNumber": int(item["couponNumber"]),
3879                        "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3880                        "payCurrency": item["payOneBond"]["currency"],
3881                        "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3882                        "couponType": TKS_COUPON_TYPES[item["couponType"]],
3883                        "couponStartDate": item["couponStartDate"],
3884                        "couponEndDate": item["couponEndDate"],
3885                        "couponPeriod": item["couponPeriod"],
3886                    })
3887
3888                # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3889                if "maturityDate" not in iData.keys():
3890                    iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3891
3892                # Widen raw data with Coupon Rate.
3893                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3894                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3895                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3896                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3897
3898                # Widen raw data with Yield to Maturity (YTM) on current date.
3899                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3900                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3901                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3902                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3903                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3904                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3905
3906                iData["calendar"] = calendarData  # adds calendar at the end
3907
3908                # Remove not used data:
3909                iData.pop("uid")
3910                iData.pop("positionUid")
3911                iData.pop("currentPrice")
3912                iData.pop("rawCalendar")
3913
3914                colNames = list(iData.keys())
3915                if bonds is None:
3916                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3917
3918                else:
3919                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3920
3921            else:
3922                uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"]))
3923
3924            processed = round(100 * (i + 1) / iCount, 1)
3925            if tooLong and processed % 5 == 0:
3926                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3927
3928            else:
3929                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3930
3931        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3932
3933        # Saving bonds from pandas dataframe to XLSX sheet:
3934        if xlsx and self.bondsXLSXFile:
3935            with pd.ExcelWriter(
3936                    path=self.bondsXLSXFile,
3937                    date_format=TKS_DATE_FORMAT,
3938                    datetime_format=TKS_DATE_TIME_FORMAT,
3939                    mode="w",
3940            ) as writer:
3941                bonds.to_excel(
3942                    writer,
3943                    sheet_name="Extended bonds data",
3944                    index=True,
3945                    encoding="UTF-8",
3946                    freeze_panes=(1, 1),
3947                )  # saving as XLSX-file with freeze first row and column as headers
3948
3949            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3950
3951        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports pandas dataframe to xlsx-file bondsXLSXFile, default: ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider pandas dataframe with more full and calculated data about bonds, than raw response from broker. In XLSX-file and pandas dataframe fields mean: https://tinkoff.github.io/investAPI/instruments/#bond

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
3953    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3954        """
3955        Creates bond payments calendar as pandas dataframe, and you can also save it to the XLSX-file.
3956
3957        WARNING! This is too long operation if a lot of bonds requested from broker server.
3958
3959        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3960
3961        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
3962                        extended information about bonds: main info, current prices, bond payment calendar,
3963                        coupon yields, current yields and some statistics etc.
3964                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3965        :param xlsx: if True then also exports pandas dataframe to file `calendarFile` + `".xlsx"`, default: `calendar.xlsx`,
3966                     for further used by data scientists or stock analytics.
3967        :return: pandas dataframe with only bond payments calendar data.
3968        """
3969        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3970            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3971
3972        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3973
3974        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3975        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3976        calendar = None
3977        for bond in extBonds.iterrows():
3978            for item in bond[1]["calendar"]:
3979                cData = {
3980                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3981                    "couponDate": item["couponDate"],
3982                    "figi": bond[1]["figi"],
3983                    "ticker": bond[1]["ticker"],
3984                    "name": bond[1]["name"],
3985                    "couponNumber": item["couponNumber"],
3986                    "payOneBond": item["payOneBond"],
3987                    "payCurrency": item["payCurrency"],
3988                    "couponType": item["couponType"],
3989                    "couponPeriod": item["couponPeriod"],
3990                    "fixDate": item["fixDate"],
3991                    "couponStartDate": item["couponStartDate"],
3992                    "couponEndDate": item["couponEndDate"],
3993                }
3994
3995                if calendar is None:
3996                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
3997
3998                else:
3999                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4000
4001        calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4002
4003        # Saving calendar from pandas dataframe to XLSX sheet:
4004        if xlsx:
4005            xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4006
4007            with pd.ExcelWriter(
4008                    path=xlsxCalendarFile,
4009                    date_format=TKS_DATE_FORMAT,
4010                    datetime_format=TKS_DATE_TIME_FORMAT,
4011                    mode="w",
4012            ) as writer:
4013                humanReadable = calendar.copy(deep=True)
4014                humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4015                humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4016                humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4017                humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4018                humanReadable.columns = colNames  # human-readable column names
4019
4020                humanReadable.to_excel(
4021                    writer,
4022                    sheet_name="Bond payments calendar",
4023                    index=False,
4024                    encoding="UTF-8",
4025                    freeze_panes=(1, 2),
4026                )  # saving as XLSX-file with freeze first row and column as headers
4027
4028                del humanReadable  # release df in memory
4029
4030            uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4031
4032        return calendar

Creates bond payments calendar as pandas dataframe, and you can also save it to the XLSX-file.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: pandas dataframe object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports pandas dataframe to file calendarFile + ".xlsx", default: calendar.xlsx, for further used by data scientists or stock analytics.
Returns

pandas dataframe with only bond payments calendar data.

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4034    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4035        """
4036        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4037
4038        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
4039
4040        :param extBonds: pandas dataframe object returns by `ExtendBondsData()` method and contains
4041                        extended information about bonds: main info, current prices, bond payment calendar,
4042                        coupon yields, current yields and some statistics etc.
4043                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4044        :param show: if `True` then also printing bonds payment calendar to the console,
4045                     otherwise save to file `calendarFile` only. `False` by default.
4046        :return: multilines text in Markdown format with bonds payment calendar as a table.
4047        """
4048        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4049            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4050
4051        infoText = "# Bond payments calendar\n\n"
4052
4053        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate pandas dataframe with full calendar data
4054
4055        if not calendar.empty:
4056            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4057
4058            info = [
4059                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4060                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4061            ]
4062
4063            newMonth = False
4064            notOneBond = calendar["figi"].nunique() > 1
4065            for i, bond in enumerate(calendar.iterrows()):
4066                if newMonth and notOneBond:
4067                    info.append(splitLine)
4068
4069                info.append(
4070                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4071                        "  +" if bond[1]["paid"] else "  —",
4072                        bond[1]["couponDate"].split("T")[0],
4073                        bond[1]["figi"],
4074                        bond[1]["ticker"],
4075                        bond[1]["couponNumber"],
4076                        "{} {}".format(
4077                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4078                            bond[1]["payCurrency"],
4079                        ),
4080                        bond[1]["couponType"],
4081                        bond[1]["couponPeriod"],
4082                        bond[1]["fixDate"].split("T")[0],
4083                    )
4084                )
4085
4086                if i < len(calendar.values) - 1:
4087                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4088                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4089                    newMonth = False if curDate.month == nextDate.month else True
4090
4091                else:
4092                    newMonth = False
4093
4094            infoText += "".join(info)
4095
4096            if show:
4097                uLogger.info("{}".format(infoText))
4098
4099            if self.calendarFile is not None:
4100                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4101                    fH.write(infoText)
4102
4103                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4104
4105        else:
4106            infoText += "No data\n"
4107
4108        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.

See also: ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

Parameters
  • extBonds: pandas dataframe object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4110    def OverviewAccounts(self, show: bool = False) -> dict:
4111        """
4112        Method for parsing and show simple table with all available user accounts.
4113
4114        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4115
4116        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4117        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4118                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4119                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4120                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4121                                                        "closed": "—", "access": "Full access" }, ...}}`
4122        """
4123        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4124
4125        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4126        accounts = {
4127            item["id"]: {
4128                "type": TKS_ACCOUNT_TYPES[item["type"]],
4129                "name": item["name"],
4130                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4131                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4132                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4133                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4134            } for item in rawAccounts["accounts"]
4135        }
4136
4137        # Raw and parsed data with some fields replaced in "stat" section:
4138        view = {
4139            "rawAccounts": rawAccounts,
4140            "stat": accounts,
4141        }
4142
4143        # --- Prepare simple text table with only accounts data in human-readable format:
4144        if show:
4145            info = [
4146                "# User accounts\n\n",
4147                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4148                "| Account ID   | Type                      | Status                    | Name                           |\n",
4149                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4150            ]
4151
4152            for account in view["stat"].keys():
4153                info.extend([
4154                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4155                        account,
4156                        view["stat"][account]["type"],
4157                        view["stat"][account]["status"],
4158                        view["stat"][account]["name"],
4159                    )
4160                ])
4161
4162            infoText = "".join(info)
4163
4164            uLogger.info(infoText)
4165
4166            if self.userAccountsFile:
4167                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4168                    fH.write(infoText)
4169
4170                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4171
4172        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4174    def OverviewUserInfo(self, show: bool = False) -> dict:
4175        """
4176        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4177
4178        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4179
4180        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4181        :return: dict with raw parsed data from server and some calculated statistics about it.
4182        """
4183        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4184        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4185        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4186        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4187        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4188        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4189
4190        # This is dict with parsed common user data:
4191        userInfo = {
4192            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4193            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4194            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4195            "tariff": rawUserInfo["tariff"],
4196        }
4197
4198        # This is an array of dict with parsed margin statuses for every account IDs:
4199        margins = {}
4200        for accountId in accounts.keys():
4201            if rawMargins[accountId]:
4202                margins[accountId] = {
4203                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4204                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4205                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4206                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4207                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4208                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4209                }
4210
4211            else:
4212                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4213
4214        unary = {}  # unary-connection limits
4215        for item in rawTariffLimits["unaryLimits"]:
4216            if item["limitPerMinute"] in unary.keys():
4217                unary[item["limitPerMinute"]].extend(item["methods"])
4218
4219            else:
4220                unary[item["limitPerMinute"]] = item["methods"]
4221
4222        stream = {}  # stream-connection limits
4223        for item in rawTariffLimits["streamLimits"]:
4224            if item["limit"] in stream.keys():
4225                stream[item["limit"]].extend(item["streams"])
4226
4227            else:
4228                stream[item["limit"]] = item["streams"]
4229
4230        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4231        limits = {
4232            "unary": unary,
4233            "stream": stream,
4234        }
4235
4236        # Raw and parsed data as an output result:
4237        view = {
4238            "rawUserInfo": rawUserInfo,
4239            "rawAccounts": rawAccounts,
4240            "rawMargins": rawMargins,
4241            "rawTariffLimits": rawTariffLimits,
4242            "stat": {
4243                "userInfo": userInfo,
4244                "accounts": accounts,
4245                "margins": margins,
4246                "limits": limits,
4247            },
4248        }
4249
4250        # --- Prepare text table with user information in human-readable format:
4251        if show:
4252            info = [
4253                "# Full user information\n\n",
4254                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4255                "## Common information\n\n",
4256                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4257                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4258                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4259                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4260                "\n## User accounts\n\n",
4261            ]
4262
4263            for account in view["stat"]["accounts"].keys():
4264                info.extend([
4265                    "### ID: [{}]\n\n".format(account),
4266                    "| Parameters           | Values                                                       |\n",
4267                    "|----------------------|--------------------------------------------------------------|\n",
4268                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4269                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4270                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4271                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4272                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4273                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4274                ])
4275
4276                if margins[account]:
4277                    info.extend([
4278                        "| Margin status:       | Enabled                                                      |\n",
4279                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4280                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4281                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4282                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4283                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4284                    ])
4285
4286                else:
4287                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4288
4289            info.extend([
4290                "\n## Current user tariff limits\n",
4291                "\nSee also:\n",
4292                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4293                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4294                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4295                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4296                "\n### Unary limits\n",
4297            ])
4298
4299            if unary:
4300                for key, values in sorted(unary.items()):
4301                    info.append("\n* Max requests per minute: {}\n".format(key))
4302
4303                    for value in values:
4304                        info.append("  - {}\n".format(value))
4305
4306            else:
4307                info.append("\nNot available\n")
4308
4309            info.append("\n### Stream limits\n")
4310
4311            if stream:
4312                for key, values in sorted(stream.items()):
4313                    info.append("\n* Max stream connections: {}\n".format(key))
4314
4315                    for value in values:
4316                        info.append("  - {}\n".format(value))
4317
4318            else:
4319                info.append("\nNot available\n")
4320
4321            infoText = "".join(info)
4322
4323            uLogger.info(infoText)
4324
4325            if self.userInfoFile:
4326                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4327                    fH.write(infoText)
4328
4329                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4330
4331        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4334class Args:
4335    """
4336    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4337    """
4338    def __init__(self, **kwargs):
4339        self.__dict__.update(kwargs)
4340
4341    def __getattr__(self, item):
4342        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4338    def __init__(self, **kwargs):
4339        self.__dict__.update(kwargs)
def ParseArgs()
4345def ParseArgs():
4346    """
4347    Function get and parse command line keys. See examples: https://tim55667757.github.io/TKSBrokerAPI/
4348    """
4349    parser = ArgumentParser()  # command-line string parser
4350
4351    parser.description = "TKSBrokerAPI is a python API to work with some methods of Tinkoff Open API using REST protocol. It can view history, orders and market information. Also, you can open orders and trades. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples"
4352    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4353
4354    # --- options:
4355
4356    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the program. `False` by default.")
4357    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4358    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4359
4360    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4361    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4362
4363    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4364    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4365
4366    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4367
4368    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4369    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4370    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4371
4372    parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4373
4374    # --- commands:
4375
4376    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4377
4378    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4379    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4380    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider pandas dataframe with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to xlsx-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4381    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4382    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4383    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present, and also saved to file `calendar.xlsx` by default. Also, if the `--output` key present then calendar saves to file, default: `calendar.md`. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4384    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4385    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4386
4387    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4388    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4389    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4390    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4391    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4392
4393    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4394    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4395    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4396    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4397
4398    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4399    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4400    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4401
4402    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4403    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4404    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4405    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4406    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4407    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4408    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4409
4410    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4411    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4412    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.")
4413    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.")
4414    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4415
4416    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4417    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4418    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4419
4420    cmdArgs = parser.parse_args()
4421    return cmdArgs

Function get and parse command line keys. See examples: https://tim55667757.github.io/TKSBrokerAPI/

def Main(**kwargs)
4424def Main(**kwargs):
4425    """
4426    Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.
4427
4428    See examples: https://tim55667757.github.io/TKSBrokerAPI/
4429    """
4430    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4431
4432    if args.debug_level:
4433        uLogger.level = 10  # always debug level by default
4434        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4435
4436    exitCode = 0
4437    start = datetime.now(tzutc())
4438    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4439        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4440        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4441    ))
4442
4443    # trying to calculate full current version:
4444    buildVersion = __version__
4445    try:
4446        v = version("tksbrokerapi")
4447        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4448
4449    except Exception:
4450        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4451
4452    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4453    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4454
4455    try:
4456        if args.version:
4457            print("TKSBrokerAPI {}".format(buildVersion))
4458            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4459
4460        else:
4461            # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader`
4462            server = TinkoffBrokerServer(
4463                token=args.token,
4464                accountId=args.account_id,
4465                useCache=not args.no_cache,
4466            )
4467
4468            # --- set some options:
4469
4470            if args.ticker:
4471                if args.ticker in server.aliasesKeys:
4472                    server.ticker = server.aliases[args.ticker]  # Replace some tickers with its aliases
4473
4474                else:
4475                    server.ticker = args.ticker
4476
4477            if args.figi:
4478                server.figi = args.figi
4479
4480            if args.depth is not None:
4481                server.depth = args.depth
4482
4483            # --- do one of commands:
4484
4485            if args.list:
4486                if args.output is not None:
4487                    server.instrumentsFile = args.output
4488
4489                server.ShowInstrumentsInfo(show=True)
4490
4491            elif args.list_xlsx:
4492                server.DumpInstrumentsAsXLSX(forceUpdate=False)
4493
4494            elif args.bonds_xlsx is not None:
4495                if args.output is not None:
4496                    server.bondsXLSXFile = args.output
4497
4498                if len(args.bonds_xlsx) == 0:
4499                    server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4500
4501                else:
4502                    server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4503
4504            elif args.search:
4505                if args.output is not None:
4506                    server.searchResultsFile = args.output
4507
4508                server.SearchInstruments(pattern=args.search[0], show=True)
4509
4510            elif args.info:
4511                if not (args.ticker or args.figi):
4512                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4513                    raise Exception("Ticker or FIGI required")
4514
4515                if args.output is not None:
4516                    server.infoFile = args.output
4517
4518                if args.ticker:
4519                    server.SearchByTicker(requestPrice=True, show=True, debug=False)  # show info and current prices by ticker name
4520
4521                else:
4522                    server.SearchByFIGI(requestPrice=True, show=True, debug=False)  # show info and current prices by FIGI id
4523
4524            elif args.calendar is not None:
4525                if args.output is not None:
4526                    server.calendarFile = args.output
4527
4528                if len(args.calendar) == 0:
4529                    bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4530
4531                else:
4532                    bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4533
4534                server.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4535
4536            elif args.price:
4537                if not (args.ticker or args.figi):
4538                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4539                    raise Exception("Ticker or FIGI required")
4540
4541                server.GetCurrentPrices(show=True)
4542
4543            elif args.prices is not None:
4544                if args.output is not None:
4545                    server.pricesFile = args.output
4546
4547                server.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4548
4549            elif args.overview:
4550                if args.output is not None:
4551                    server.overviewFile = args.output
4552
4553                server.Overview(show=True, details="full")
4554
4555            elif args.overview_digest:
4556                if args.output is not None:
4557                    server.overviewDigestFile = args.output
4558
4559                server.Overview(show=True, details="digest")
4560
4561            elif args.overview_positions:
4562                if args.output is not None:
4563                    server.overviewPositionsFile = args.output
4564
4565                server.Overview(show=True, details="positions")
4566
4567            elif args.overview_orders:
4568                if args.output is not None:
4569                    server.overviewOrdersFile = args.output
4570
4571                server.Overview(show=True, details="orders")
4572
4573            elif args.overview_analytics:
4574                if args.output is not None:
4575                    server.overviewAnalyticsFile = args.output
4576
4577                server.Overview(show=True, details="analytics")
4578
4579            elif args.deals is not None:
4580                if args.output is not None:
4581                    server.reportFile = args.output
4582
4583                if 0 <= len(args.deals) < 3:
4584                    server.Deals(
4585                        start=args.deals[0] if len(args.deals) >= 1 else None,
4586                        end=args.deals[1] if len(args.deals) == 2 else None,
4587                        show=True,  # Always show deals report in console
4588                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4589                    )
4590
4591                else:
4592                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4593                    raise Exception("Incorrect value")
4594
4595            elif args.history is not None:
4596                if args.output is not None:
4597                    server.historyFile = args.output
4598
4599                if 0 <= len(args.history) < 3:
4600                    dataReceived = server.History(
4601                        start=args.history[0] if len(args.history) >= 1 else None,
4602                        end=args.history[1] if len(args.history) == 2 else None,
4603                        interval="hour" if args.interval is None or not args.interval else args.interval,
4604                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4605                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4606                        show=True,  # shows all downloaded candles in console
4607                    )
4608
4609                    if args.render_chart is not None and dataReceived is not None:
4610                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4611
4612                        server.ShowHistoryChart(
4613                            candles=dataReceived,
4614                            interact=iChart,
4615                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4616                        )
4617
4618                else:
4619                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4620                    raise Exception("Incorrect value")
4621
4622            elif args.load_history is not None:
4623                histData = server.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4624
4625                if args.render_chart is not None and histData is not None:
4626                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4627                    server.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4628
4629                    server.ShowHistoryChart(
4630                        candles=histData,
4631                        interact=iChart,
4632                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4633                    )
4634
4635            elif args.trade is not None:
4636                if 1 <= len(args.trade) <= 5:
4637                    server.Trade(
4638                        operation=args.trade[0],
4639                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4640                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4641                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4642                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4643                    )
4644
4645                else:
4646                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4647
4648            elif args.buy is not None:
4649                if 0 <= len(args.buy) <= 4:
4650                    server.Buy(
4651                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4652                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4653                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4654                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4655                    )
4656
4657                else:
4658                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4659
4660            elif args.sell is not None:
4661                if 0 <= len(args.sell) <= 4:
4662                    server.Sell(
4663                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4664                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4665                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4666                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4667                    )
4668
4669                else:
4670                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4671
4672            elif args.order:
4673                if 4 <= len(args.order) <= 7:
4674                    server.Order(
4675                        operation=args.order[0],
4676                        orderType=args.order[1],
4677                        lots=int(args.order[2]),
4678                        targetPrice=float(args.order[3]),
4679                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4680                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4681                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4682                    )
4683
4684                else:
4685                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4686
4687            elif args.buy_limit:
4688                server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4689
4690            elif args.sell_limit:
4691                server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4692
4693            elif args.buy_stop:
4694                if 2 <= len(args.buy_stop) <= 7:
4695                    server.BuyStop(
4696                        lots=int(args.buy_stop[0]),
4697                        targetPrice=float(args.buy_stop[1]),
4698                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4699                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4700                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4701                    )
4702
4703                else:
4704                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4705
4706            elif args.sell_stop:
4707                if 2 <= len(args.sell_stop) <= 7:
4708                    server.SellStop(
4709                        lots=int(args.sell_stop[0]),
4710                        targetPrice=float(args.sell_stop[1]),
4711                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4712                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4713                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4714                    )
4715
4716                else:
4717                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4718
4719            # elif args.buy_order_grid is not None:
4720            #     # update order grid work with api v2
4721            #     if len(args.buy_order_grid) == 2:
4722            #         orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4723            #
4724            #         for order in orderParams:
4725            #             server.Order(operation="Buy", lots=order["lot"], price=order["price"])
4726            #
4727            #     else:
4728            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4729            #
4730            # elif args.sell_order_grid is not None:
4731            #     # update order grid work with api v2
4732            #     if len(args.sell_order_grid) >= 2:
4733            #         orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4734            #
4735            #         for order in orderParams:
4736            #             server.Order(operation="Sell", lots=order["lot"], price=order["price"])
4737            #
4738            #     else:
4739            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4740
4741            elif args.close_order is not None:
4742                server.CloseOrders(args.close_order)  # close only one order
4743
4744            elif args.close_orders is not None:
4745                server.CloseOrders(args.close_orders)  # close list of orders
4746
4747            elif args.close_trade:
4748                if not args.ticker:
4749                    uLogger.error("`--ticker` key is required for this operation!")
4750                    raise Exception("Ticker required")
4751
4752                server.CloseTrades([args.ticker])  # close only one trade
4753
4754            elif args.close_trades is not None:
4755                server.CloseTrades(args.close_trades)  # close trades for list of tickers
4756
4757            elif args.close_all is not None:
4758                server.CloseAll(*args.close_all)
4759
4760            elif args.limits:
4761                if args.output is not None:
4762                    server.withdrawalLimitsFile = args.output
4763
4764                server.OverviewLimits(show=True)
4765
4766            elif args.user_info:
4767                if args.output is not None:
4768                    server.userInfoFile = args.output
4769
4770                server.OverviewUserInfo(show=True)
4771
4772            elif args.account:
4773                if args.output is not None:
4774                    server.userAccountsFile = args.output
4775
4776                server.OverviewAccounts(show=True)
4777
4778            else:
4779                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4780                raise Exception("There is no command to execute")
4781
4782    except Exception:
4783        trace = tb.format_exc()
4784        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4785            if e in trace:
4786                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4787                break
4788
4789        uLogger.debug(trace)
4790        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4791        exitCode = 255  # an error occurred, must be open a ticket for this issue
4792
4793    finally:
4794        finish = datetime.now(tzutc())
4795
4796        if exitCode == 0:
4797            uLogger.debug("All operations were finished success (summary code is 0).")
4798
4799        else:
4800            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4801                os.path.abspath(uLog.defaultLogFile), exitCode,
4802            ))
4803
4804        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4805        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4806            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4807            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4808        ))
4809
4810        if not kwargs:
4811            sys.exit(exitCode)
4812
4813        else:
4814            return exitCode

Main function for work with Tinkoff Open API service. It realizes simple logic: get a lot of options and execute one command.

See examples: https://tim55667757.github.io/TKSBrokerAPI/